posterly-mcp-server 0.44.0 → 0.45.0

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 (155) hide show
  1. package/README.md +310 -111
  2. package/dist/index.js +190 -894
  3. package/dist/lib/alias-transport.d.ts +84 -0
  4. package/dist/lib/alias-transport.js +193 -0
  5. package/dist/lib/api-client.d.ts +18 -0
  6. package/dist/lib/api-client.js +55 -0
  7. package/dist/lib/preview-token-core.d.ts +62 -0
  8. package/dist/lib/preview-token-core.js +131 -0
  9. package/dist/lib/preview-token.d.ts +23 -0
  10. package/dist/lib/preview-token.js +44 -0
  11. package/dist/lib/tool-registry.d.ts +246 -0
  12. package/dist/lib/tool-registry.js +394 -0
  13. package/dist/lib/tool-runtime.d.ts +68 -0
  14. package/dist/lib/tool-runtime.js +85 -0
  15. package/dist/lib/version.d.ts +1 -1
  16. package/dist/lib/version.js +1 -1
  17. package/dist/tools/audit-google-business-profile.js +1 -1
  18. package/dist/tools/connect-account.d.ts +4 -4
  19. package/dist/tools/connect-account.js +2 -2
  20. package/dist/tools/create-connect-session.d.ts +2 -2
  21. package/dist/tools/create-connect-session.js +2 -2
  22. package/dist/tools/create-post.d.ts +6 -6
  23. package/dist/tools/create-post.js +3 -3
  24. package/dist/tools/create-posts-batch.d.ts +4 -4
  25. package/dist/tools/create-webhook.d.ts +2 -2
  26. package/dist/tools/delete-post.d.ts +34 -10
  27. package/dist/tools/delete-post.js +52 -16
  28. package/dist/tools/generate-captions.d.ts +2 -2
  29. package/dist/tools/generate-image.d.ts +4 -4
  30. package/dist/tools/generate-image.js +2 -2
  31. package/dist/tools/generate-video.d.ts +6 -6
  32. package/dist/tools/generate-video.js +1 -1
  33. package/dist/tools/get-agent-signup-info.js +2 -2
  34. package/dist/tools/get-post.d.ts +16 -1
  35. package/dist/tools/get-post.js +35 -19
  36. package/dist/tools/list-accounts.d.ts +49 -3
  37. package/dist/tools/list-accounts.js +148 -33
  38. package/dist/tools/list-analytics.d.ts +111 -0
  39. package/dist/tools/list-analytics.js +393 -0
  40. package/dist/tools/list-brands.d.ts +38 -3
  41. package/dist/tools/list-brands.js +138 -20
  42. package/dist/tools/list-comments.d.ts +29 -9
  43. package/dist/tools/list-comments.js +75 -34
  44. package/dist/tools/list-conversations.d.ts +32 -9
  45. package/dist/tools/list-conversations.js +63 -27
  46. package/dist/tools/list-google-business-media.js +1 -1
  47. package/dist/tools/list-google-business-reviews.d.ts +32 -9
  48. package/dist/tools/list-google-business-reviews.js +77 -52
  49. package/dist/tools/list-jobs.d.ts +60 -0
  50. package/dist/tools/list-jobs.js +81 -0
  51. package/dist/tools/list-platforms.d.ts +62 -3
  52. package/dist/tools/list-platforms.js +118 -9
  53. package/dist/tools/list-posts.d.ts +2 -2
  54. package/dist/tools/manage-comment.d.ts +102 -0
  55. package/dist/tools/manage-comment.js +125 -0
  56. package/dist/tools/manage-conversation.d.ts +79 -0
  57. package/dist/tools/manage-conversation.js +75 -0
  58. package/dist/tools/manage-google-business-media.d.ts +103 -0
  59. package/dist/tools/manage-google-business-media.js +126 -0
  60. package/dist/tools/manage-google-business-review.d.ts +146 -0
  61. package/dist/tools/manage-google-business-review.js +115 -0
  62. package/dist/tools/manage-oauth-client.d.ts +121 -0
  63. package/dist/tools/manage-oauth-client.js +94 -0
  64. package/dist/tools/manage-webhook.d.ts +99 -0
  65. package/dist/tools/manage-webhook.js +87 -0
  66. package/dist/tools/start-signup.d.ts +2 -2
  67. package/dist/tools/submit-agent-feedback.d.ts +4 -4
  68. package/dist/tools/trigger-platform-helper.d.ts +2 -2
  69. package/dist/tools/update-post.d.ts +62 -11
  70. package/dist/tools/update-post.js +79 -26
  71. package/dist/tools/upload-media.d.ts +32 -9
  72. package/dist/tools/upload-media.js +54 -18
  73. package/dist/tools/validate-post.d.ts +4 -4
  74. package/dist/tools/whoami.d.ts +15 -2
  75. package/dist/tools/whoami.js +221 -25
  76. package/package.json +5 -3
  77. package/scripts/test-alias-transport.mjs +368 -0
  78. package/scripts/test-client-header-sanitization.mjs +78 -0
  79. package/scripts/test-tool-annotations.mjs +38 -17
  80. package/server.json +9 -2
  81. package/src/index.ts +227 -1443
  82. package/src/lib/alias-transport.ts +233 -0
  83. package/src/lib/api-client.ts +65 -0
  84. package/src/lib/preview-token-core.ts +157 -0
  85. package/src/lib/preview-token.ts +54 -0
  86. package/src/lib/tool-registry.ts +538 -0
  87. package/src/lib/tool-runtime.ts +117 -0
  88. package/src/lib/version.ts +1 -1
  89. package/src/tools/audit-google-business-profile.ts +1 -1
  90. package/src/tools/connect-account.ts +2 -2
  91. package/src/tools/create-connect-session.ts +2 -2
  92. package/src/tools/create-post.ts +3 -3
  93. package/src/tools/delete-post.ts +60 -19
  94. package/src/tools/generate-image.ts +2 -2
  95. package/src/tools/generate-video.ts +1 -1
  96. package/src/tools/get-agent-signup-info.ts +2 -2
  97. package/src/tools/get-post.ts +35 -21
  98. package/src/tools/list-accounts.ts +187 -39
  99. package/src/tools/list-analytics.ts +444 -0
  100. package/src/tools/list-brands.ts +162 -26
  101. package/src/tools/list-comments.ts +86 -43
  102. package/src/tools/list-conversations.ts +81 -39
  103. package/src/tools/list-google-business-media.ts +1 -1
  104. package/src/tools/list-google-business-reviews.ts +93 -64
  105. package/src/tools/list-jobs.ts +91 -0
  106. package/src/tools/list-platforms.ts +135 -11
  107. package/src/tools/manage-comment.ts +142 -0
  108. package/src/tools/manage-conversation.ts +89 -0
  109. package/src/tools/manage-google-business-media.ts +147 -0
  110. package/src/tools/manage-google-business-review.ts +139 -0
  111. package/src/tools/manage-oauth-client.ts +116 -0
  112. package/src/tools/manage-webhook.ts +103 -0
  113. package/src/tools/update-post.ts +106 -39
  114. package/src/tools/upload-media.ts +76 -28
  115. package/src/tools/whoami.ts +253 -30
  116. package/tool-annotations.json +76 -149
  117. package/tool-registry.json +2085 -0
  118. package/src/tools/add-google-business-media.ts +0 -50
  119. package/src/tools/create-oauth-client.ts +0 -36
  120. package/src/tools/delete-comment.ts +0 -29
  121. package/src/tools/delete-google-business-media.ts +0 -30
  122. package/src/tools/delete-google-business-review-reply.ts +0 -30
  123. package/src/tools/delete-oauth-client.ts +0 -23
  124. package/src/tools/delete-post-group.ts +0 -17
  125. package/src/tools/delete-webhook.ts +0 -17
  126. package/src/tools/get-account-analytics.ts +0 -171
  127. package/src/tools/get-brand-profile.ts +0 -86
  128. package/src/tools/get-brand.ts +0 -34
  129. package/src/tools/get-comment.ts +0 -47
  130. package/src/tools/get-connect-link.ts +0 -61
  131. package/src/tools/get-connect-session.ts +0 -63
  132. package/src/tools/get-conversation.ts +0 -44
  133. package/src/tools/get-google-business-review-link.ts +0 -30
  134. package/src/tools/get-image-job.ts +0 -36
  135. package/src/tools/get-learned-voice.ts +0 -31
  136. package/src/tools/get-mcp-status.ts +0 -212
  137. package/src/tools/get-performance-profile.ts +0 -47
  138. package/src/tools/get-platform-schema.ts +0 -56
  139. package/src/tools/get-post-analytics.ts +0 -243
  140. package/src/tools/get-post-insights.ts +0 -60
  141. package/src/tools/get-post-missing.ts +0 -16
  142. package/src/tools/get-video-job.ts +0 -36
  143. package/src/tools/list-brand-accounts.ts +0 -36
  144. package/src/tools/reply-google-business-review.ts +0 -32
  145. package/src/tools/reply-to-comment.ts +0 -30
  146. package/src/tools/send-message.ts +0 -28
  147. package/src/tools/suggest-google-business-review-reply.ts +0 -44
  148. package/src/tools/sync-inbox.ts +0 -28
  149. package/src/tools/test-webhook.ts +0 -18
  150. package/src/tools/update-comment.ts +0 -31
  151. package/src/tools/update-oauth-client.ts +0 -38
  152. package/src/tools/update-post-release-id.ts +0 -25
  153. package/src/tools/update-post-status.ts +0 -31
  154. package/src/tools/update-webhook.ts +0 -36
  155. package/src/tools/upload-media-from-url.ts +0 -33
@@ -0,0 +1,393 @@
1
+ import { z } from 'zod';
2
+ import { formatDateTime, mdEmpty, mdKeyValue, mdSection, mdTitle, truncateText } from '../lib/format.js';
3
+ import { validateArgs } from '../lib/tool-runtime.js';
4
+ const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
5
+ export const listAnalyticsTool = {
6
+ name: 'list_analytics',
7
+ description: 'Analytics for connected social accounts (Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, YouTube, and Threads). view=accounts (account_id): daily snapshots and a period summary with platform-native display_metrics labels. view=posts (account_id): per-post likes, comments/replies, reach, impressions/views, saves, shares, reposts, quotes, plays, clicks, and watch time, most recent first, with limit/offset. view=insights: per-post feedback insights with a performance tier (great/good/mixed/poor), diagnosis, next action, and baseline, filtered by account_id, post_id, or checkpoint (1h, 6h, 24h, 72h, 7d); read-only, Pro plan or higher. presentation (accounts and posts): compact for Telegram/mobile, table for Markdown clients, json for custom chart/card renderers.',
8
+ inputSchema: z.object({
9
+ view: z.enum(['accounts', 'posts', 'insights']).describe('accounts, posts, or insights.'),
10
+ account_id: z
11
+ .union([z.number(), z.string()])
12
+ .optional()
13
+ .describe('The social account ID (from list_accounts). Required for accounts and posts; optional filter for insights.'),
14
+ from: z
15
+ .string()
16
+ .optional()
17
+ .describe('accounts and posts: start date (ISO date, e.g. 2026-03-19). Defaults to 30 days ago.'),
18
+ to: z
19
+ .string()
20
+ .optional()
21
+ .describe('accounts and posts: end date (ISO date). Defaults to today.'),
22
+ limit: z
23
+ .number()
24
+ .min(1)
25
+ .max(200)
26
+ .optional()
27
+ .describe('posts: number of posts (default 50, max 200). insights: max insights (default 20, max 100).'),
28
+ offset: z.number().min(0).optional().describe('posts: pagination offset'),
29
+ post_id: z.number().int().positive().optional().describe('insights: filter to one post ID.'),
30
+ checkpoint: z
31
+ .enum(['1h', '6h', '24h', '72h', '7d'])
32
+ .optional()
33
+ .describe('insights: filter to one checkpoint after publish.'),
34
+ presentation: presentationSchema.describe('accounts and posts: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
35
+ }),
36
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
37
+ legacySchemas: {
38
+ get_account_analytics: z.object({
39
+ account_id: z.number(),
40
+ from: z.string().optional(),
41
+ to: z.string().optional(),
42
+ presentation: presentationSchema,
43
+ }),
44
+ get_post_analytics: z.object({
45
+ account_id: z.number(),
46
+ from: z.string().optional(),
47
+ to: z.string().optional(),
48
+ limit: z.number().min(1).max(200).optional(),
49
+ offset: z.number().min(0).optional(),
50
+ presentation: presentationSchema,
51
+ }),
52
+ get_post_insights: z.object({
53
+ account_id: z.union([z.string(), z.number()]).optional(),
54
+ post_id: z.number().int().positive().optional(),
55
+ checkpoint: z.enum(['1h', '6h', '24h', '72h', '7d']).optional(),
56
+ limit: z.number().int().min(1).max(100).optional(),
57
+ }),
58
+ },
59
+ async execute(client, input, ctx) {
60
+ const { mode, args } = validateArgs(ctx, 'list_analytics', input);
61
+ if (mode === 'accounts') {
62
+ const result = await client.getAccountAnalytics(args);
63
+ return formatAccountAnalytics(result, args.presentation || 'compact');
64
+ }
65
+ if (mode === 'posts') {
66
+ const result = await client.getPostAnalytics(args);
67
+ return formatPostAnalytics(result, args.presentation || 'compact');
68
+ }
69
+ return postInsights(client, args);
70
+ },
71
+ };
72
+ async function postInsights(client, input) {
73
+ const { insights } = await client.getPostInsights(input);
74
+ if (!insights.length) {
75
+ return mdEmpty('feedback insights', 'No per-post feedback insights are available for these filters yet. Insights appear as posts hit their 1h, 6h, 24h, 72h, and 7d checkpoints.');
76
+ }
77
+ const blocks = insights.map((insight) => {
78
+ const heading = `${insight.title || 'Insight'} (${insight.performance_tier}, ${insight.checkpoint})`;
79
+ return mdSection(heading, mdKeyValue([
80
+ ['Post ID', insight.post_id],
81
+ ['Platform', insight.platform],
82
+ ['Account', insight.username ? `@${insight.username}` : String(insight.social_account_id)],
83
+ ['Diagnosis', truncateText(insight.diagnosis, 220)],
84
+ ['Next action', truncateText(insight.next_action, 220)],
85
+ ['Impact score', insight.impact_score],
86
+ ['Confidence', insight.confidence],
87
+ ['Published', formatDateTime(insight.published_at)],
88
+ ['Permalink', insight.permalink || undefined],
89
+ ]));
90
+ });
91
+ return [mdTitle(`Post feedback insights (${insights.length})`), ...blocks].join('\n\n');
92
+ }
93
+ function formatAccountAnalytics(result, presentation) {
94
+ const { account, range, summary, snapshots } = result;
95
+ const rows = accountMetricRows(summary, account.platform);
96
+ const insights = analyticsInsights(summary, account.platform);
97
+ if (presentation === 'json') {
98
+ return JSON.stringify({
99
+ type: 'account_analytics',
100
+ account,
101
+ range,
102
+ snapshot_count: snapshots.length,
103
+ metrics: rows.map(([label, value]) => ({ label, value })),
104
+ insights,
105
+ summary,
106
+ snapshots,
107
+ }, null, 2);
108
+ }
109
+ if (presentation === 'table') {
110
+ return [
111
+ `**📊 Analytics for ${accountLabel(account.username)} (${account.id})**`,
112
+ `_${account.platform} · ${range.from} → ${range.to}_`,
113
+ '',
114
+ '**Account**',
115
+ markdownTable(['Field', 'Value'], [
116
+ ['Account ID', String(account.id)],
117
+ ['Daily snapshots', formatNumber(snapshots.length)],
118
+ ]),
119
+ '',
120
+ '**Metrics**',
121
+ markdownTable(['Metric', 'Value'], rows),
122
+ '',
123
+ '**Quick read**',
124
+ insights.map((insight) => `- ${insight}`).join('\n'),
125
+ ].join('\n');
126
+ }
127
+ return [
128
+ `**📊 Analytics for ${accountLabel(account.username)} (${account.id})**`,
129
+ `_${account.platform} · ${range.from} → ${range.to}_`,
130
+ '',
131
+ '**Account**',
132
+ `- **Account ID:** ${account.id}`,
133
+ `- **Daily snapshots:** ${formatNumber(snapshots.length)}`,
134
+ '',
135
+ '**Key metrics**',
136
+ rows.map(([label, value]) => `- **${label}:** ${value}`).join('\n'),
137
+ '',
138
+ '**Quick read**',
139
+ insights.map((insight) => `- ${insight}`).join('\n'),
140
+ ].join('\n');
141
+ }
142
+ function accountMetricRows(summary, platform) {
143
+ const nativeRows = nativeMetricRows(summary);
144
+ if (nativeRows.length > 0)
145
+ return nativeRows;
146
+ if (platform === 'google_business') {
147
+ const metrics = summary.platform_metrics || {};
148
+ return [
149
+ ['Profile Views', formatNumber(metrics.profile_views ?? summary.total_views)],
150
+ ['Search Views', formatNumber(metrics.search_views ?? summary.total_reach)],
151
+ ['Maps Views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
152
+ ['Customer Actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
153
+ ['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
154
+ ];
155
+ }
156
+ const rows = genericMetricRows(summary);
157
+ if (platform === 'pinterest')
158
+ rows.push(['Outbound clicks', formatNumber(summary.total_website_clicks)]);
159
+ if (platform === 'youtube') {
160
+ rows.push(['Watch minutes', formatNumber(summary.total_watch_minutes)]);
161
+ rows.push(['Likes', formatNumber(summary.total_likes)]);
162
+ }
163
+ return rows;
164
+ }
165
+ function nativeMetricRows(summary) {
166
+ return (summary.display_metrics || [])
167
+ .filter((metric) => metric?.label)
168
+ .map((metric) => [metric.label, formatNumber(metric.value)]);
169
+ }
170
+ function genericMetricRows(summary) {
171
+ return [
172
+ ['Followers', `${formatNumber(summary.current_followers)} (${formatDelta(summary.followers_change)} in range)`],
173
+ ['Follows gained / lost', `+${formatNumber(summary.total_follows_gained)} / -${formatNumber(summary.total_follows_lost)}`],
174
+ ['Total reach', formatNumber(summary.total_reach)],
175
+ ['Total views', formatNumber(summary.total_views)],
176
+ ['Profile views', formatNumber(summary.total_profile_views)],
177
+ ['Accounts engaged', formatNumber(summary.total_accounts_engaged)],
178
+ ['Engagement rate by reach', formatPercent(summary.engagement_rate)],
179
+ ['Engagement rate by followers', formatPercent(summary.engagement_rate_by_followers)],
180
+ ];
181
+ }
182
+ function analyticsInsights(summary, platform) {
183
+ const insights = [];
184
+ if (platform === 'google_business' && (summary.display_metrics?.length || 0) > 0) {
185
+ insights.push('Google Business metrics are shown with dashboard-native labels.');
186
+ }
187
+ if ((summary.followers_change || 0) > 0)
188
+ insights.push(`Audience grew by ${formatDelta(summary.followers_change)} followers in this range.`);
189
+ if ((summary.followers_change || 0) < 0)
190
+ insights.push(`Audience dipped by ${formatDelta(summary.followers_change)} followers in this range.`);
191
+ if (summary.engagement_rate != null && platform !== 'google_business')
192
+ insights.push(`Engagement by reach is ${formatPercent(summary.engagement_rate)}.`);
193
+ if (platform === 'youtube' && summary.total_watch_minutes != null)
194
+ insights.push(`${formatNumber(summary.total_watch_minutes)} total watch minutes were recorded.`);
195
+ return insights.length ? insights : ['No standout movement detected in the returned period.'];
196
+ }
197
+ function formatDelta(n) {
198
+ return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
199
+ }
200
+ function formatNumber(value) {
201
+ return value == null ? 'n/a' : value.toLocaleString();
202
+ }
203
+ function formatPercent(value) {
204
+ return value == null ? 'n/a' : `${formatNumber(value)}%`;
205
+ }
206
+ function accountLabel(username) {
207
+ if (!username)
208
+ return 'account';
209
+ return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
210
+ }
211
+ function markdownTable(headers, rows) {
212
+ return [
213
+ `| ${headers.map(escapeCell).join(' | ')} |`,
214
+ `| ${headers.map(() => '---').join(' | ')} |`,
215
+ ...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
216
+ ].join('\n');
217
+ }
218
+ function escapeCell(value) {
219
+ return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
220
+ }
221
+ function formatPostAnalytics(result, presentation) {
222
+ const { account, range, posts, total } = result;
223
+ if (posts.length === 0) {
224
+ return `No analytics found for ${accountLabel(account.username)} (${account.platform}) between ${range.from} and ${range.to}.`;
225
+ }
226
+ const totals = posts.reduce((acc, post) => {
227
+ acc.likes += post.likes || 0;
228
+ acc.comments += post.comments || 0;
229
+ acc.reach += post.reach || 0;
230
+ acc.impressions += post.impressions || 0;
231
+ acc.shares += post.shares || 0;
232
+ acc.reposts += post.reposts || 0;
233
+ acc.quotes += post.quotes || 0;
234
+ acc.saves += post.saved || 0;
235
+ return acc;
236
+ }, { likes: 0, comments: 0, reach: 0, impressions: 0, shares: 0, reposts: 0, quotes: 0, saves: 0 });
237
+ const isThreads = account.platform === 'threads';
238
+ if (presentation === 'json') {
239
+ return JSON.stringify({
240
+ type: 'post_analytics',
241
+ account,
242
+ range,
243
+ returned: {
244
+ count: posts.length,
245
+ total,
246
+ },
247
+ totals,
248
+ posts,
249
+ }, null, 2);
250
+ }
251
+ if (presentation === 'table') {
252
+ if (isThreads) {
253
+ return [
254
+ `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
255
+ `_Threads · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
256
+ '',
257
+ '**Returned totals**',
258
+ markdownTable(['Views', 'Likes', 'Replies', 'Reposts', 'Quotes', 'Shares'], [[
259
+ formatNumber(totals.impressions),
260
+ formatNumber(totals.likes),
261
+ formatNumber(totals.comments),
262
+ formatNumber(totals.reposts),
263
+ formatNumber(totals.quotes),
264
+ formatNumber(totals.shares),
265
+ ]]),
266
+ '',
267
+ '**Posts**',
268
+ markdownTable(['Posted', 'Post', 'Caption', 'Views', 'Likes', 'Replies', 'Reposts', 'Quotes', 'Shares'], posts.map((post) => [
269
+ dateTime(post.posted_at),
270
+ post.id ? String(post.id) : post.platform_media_id,
271
+ compact(post.caption_snippet, 70) || '(no caption)',
272
+ formatNumber(post.impressions),
273
+ formatNumber(post.likes),
274
+ formatNumber(post.comments),
275
+ formatNumber(post.reposts),
276
+ formatNumber(post.quotes),
277
+ formatNumber(post.shares),
278
+ ])),
279
+ ].join('\n');
280
+ }
281
+ return [
282
+ `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
283
+ `_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
284
+ '',
285
+ '**Returned totals**',
286
+ markdownTable(['Likes', 'Comments', 'Reach', 'Impressions', 'Shares', 'Saves'], [[
287
+ formatNumber(totals.likes),
288
+ formatNumber(totals.comments),
289
+ formatNumber(totals.reach),
290
+ formatNumber(totals.impressions),
291
+ formatNumber(totals.shares),
292
+ formatNumber(totals.saves),
293
+ ]]),
294
+ '',
295
+ '**Posts**',
296
+ markdownTable(['Posted', 'Post', 'Caption', 'Reach', 'Likes', 'Comments', 'Extras'], posts.map((post) => [
297
+ dateTime(post.posted_at),
298
+ post.id ? String(post.id) : post.platform_media_id,
299
+ compact(post.caption_snippet, 70) || '(no caption)',
300
+ formatNumber(post.reach),
301
+ formatNumber(post.likes),
302
+ formatNumber(post.comments),
303
+ postExtras(account.platform, post),
304
+ ])),
305
+ ].join('\n');
306
+ }
307
+ const lines = [
308
+ `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
309
+ `_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
310
+ '',
311
+ '**Returned totals**',
312
+ `- **Likes:** ${formatNumber(totals.likes)}`,
313
+ `- **Comments:** ${formatNumber(totals.comments)}`,
314
+ `- **Reach:** ${formatNumber(totals.reach)}`,
315
+ `- **Impressions:** ${formatNumber(totals.impressions)}`,
316
+ `- **Shares:** ${formatNumber(totals.shares)}`,
317
+ `- **Saves:** ${formatNumber(totals.saves)}`,
318
+ '',
319
+ '**Posts**',
320
+ ];
321
+ if (isThreads) {
322
+ const threadLines = [
323
+ `**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
324
+ `_Threads · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
325
+ '',
326
+ '**Returned totals**',
327
+ `- **Views:** ${formatNumber(totals.impressions)}`,
328
+ `- **Likes:** ${formatNumber(totals.likes)}`,
329
+ `- **Replies:** ${formatNumber(totals.comments)}`,
330
+ `- **Reposts:** ${formatNumber(totals.reposts)}`,
331
+ `- **Quotes:** ${formatNumber(totals.quotes)}`,
332
+ `- **Shares:** ${formatNumber(totals.shares)}`,
333
+ '',
334
+ '**Posts**',
335
+ ];
336
+ posts.forEach((post, index) => {
337
+ threadLines.push(`${index + 1}. **${compact(post.caption_snippet, 70) || '(no caption)'}**`);
338
+ threadLines.push(` Posted: ${dateTime(post.posted_at)}`);
339
+ threadLines.push(` Post ID: ${post.id ? String(post.id) : post.platform_media_id}`);
340
+ threadLines.push(` Views: ${formatNumber(post.impressions)}`);
341
+ threadLines.push(` Likes: ${formatNumber(post.likes)}`);
342
+ threadLines.push(` Replies: ${formatNumber(post.comments)}`);
343
+ threadLines.push(` Reposts: ${formatNumber(post.reposts)}`);
344
+ threadLines.push(` Quotes: ${formatNumber(post.quotes)}`);
345
+ threadLines.push(` Shares: ${formatNumber(post.shares)}`);
346
+ if (post.permalink)
347
+ threadLines.push(` Permalink: ${post.permalink}`);
348
+ });
349
+ return threadLines.join('\n');
350
+ }
351
+ posts.forEach((post, index) => {
352
+ lines.push(`${index + 1}. **${compact(post.caption_snippet, 70) || '(no caption)'}**`);
353
+ lines.push(` Posted: ${dateTime(post.posted_at)}`);
354
+ lines.push(` Post ID: ${post.id ? String(post.id) : post.platform_media_id}`);
355
+ lines.push(` Reach: ${formatNumber(post.reach)}`);
356
+ lines.push(` Likes: ${formatNumber(post.likes)}`);
357
+ lines.push(` Comments: ${formatNumber(post.comments)}`);
358
+ lines.push(` Extras: ${postExtras(account.platform, post)}`);
359
+ });
360
+ return lines.join('\n');
361
+ }
362
+ function postExtras(platform, post) {
363
+ const metrics = [];
364
+ if (post.impressions)
365
+ metrics.push(`${formatNumber(post.impressions)} impressions`);
366
+ if (post.saved)
367
+ metrics.push(`${formatNumber(post.saved)} saved`);
368
+ if (platform !== 'threads' && post.shares)
369
+ metrics.push(`${formatNumber(post.shares)} shares`);
370
+ if (post.plays)
371
+ metrics.push(`${formatNumber(post.plays)} plays`);
372
+ if (platform === 'youtube' && post.total_watch_time_ms)
373
+ metrics.push(`${formatNumber(Math.round(post.total_watch_time_ms / 60000))} watch minutes`);
374
+ if (platform === 'youtube' && post.avg_watch_time_ms)
375
+ metrics.push(`${formatNumber(Math.round(post.avg_watch_time_ms / 1000))}s avg view`);
376
+ if (post.url_link_clicks)
377
+ metrics.push(platform === 'pinterest' ? `${formatNumber(post.url_link_clicks)} outbound clicks` : `${formatNumber(post.url_link_clicks)} link clicks`);
378
+ if (post.user_profile_clicks)
379
+ metrics.push(platform === 'pinterest' ? `${formatNumber(post.user_profile_clicks)} pin clicks` : `${formatNumber(post.user_profile_clicks)} profile clicks`);
380
+ if (post.permalink)
381
+ metrics.push(post.permalink);
382
+ return metrics.join(' · ') || 'n/a';
383
+ }
384
+ function dateTime(value) {
385
+ if (!value)
386
+ return 'n/a';
387
+ const date = new Date(value);
388
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
389
+ }
390
+ function compact(value, maxLength) {
391
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
392
+ return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
393
+ }
@@ -1,16 +1,51 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { type ToolContext } from '../lib/tool-runtime.js';
4
+ type ListBrandsInput = {
5
+ view?: 'brands' | 'brand' | 'accounts' | 'profile';
6
+ brand_id?: string;
7
+ workspace_id?: string;
8
+ };
3
9
  export declare const listBrandsTool: {
4
10
  name: string;
5
11
  description: string;
6
12
  inputSchema: z.ZodObject<{
13
+ view: z.ZodOptional<z.ZodEnum<["brands", "brand", "accounts", "profile"]>>;
14
+ brand_id: z.ZodOptional<z.ZodString>;
7
15
  workspace_id: z.ZodOptional<z.ZodString>;
8
16
  }, "strip", z.ZodTypeAny, {
9
17
  workspace_id?: string | undefined;
18
+ brand_id?: string | undefined;
19
+ view?: "accounts" | "brands" | "brand" | "profile" | undefined;
10
20
  }, {
11
21
  workspace_id?: string | undefined;
22
+ brand_id?: string | undefined;
23
+ view?: "accounts" | "brands" | "brand" | "profile" | undefined;
12
24
  }>;
13
- execute(client: PosterlyClient, input: {
14
- workspace_id?: string;
15
- }): Promise<string>;
25
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
26
+ legacySchemas: {
27
+ get_brand: z.ZodObject<{
28
+ brand_id: z.ZodString;
29
+ }, "strip", z.ZodTypeAny, {
30
+ brand_id: string;
31
+ }, {
32
+ brand_id: string;
33
+ }>;
34
+ list_brand_accounts: z.ZodObject<{
35
+ brand_id: z.ZodString;
36
+ }, "strip", z.ZodTypeAny, {
37
+ brand_id: string;
38
+ }, {
39
+ brand_id: string;
40
+ }>;
41
+ get_brand_profile: z.ZodObject<{
42
+ brand_id: z.ZodString;
43
+ }, "strip", z.ZodTypeAny, {
44
+ brand_id: string;
45
+ }, {
46
+ brand_id: string;
47
+ }>;
48
+ };
49
+ execute(client: PosterlyClient, input: ListBrandsInput, ctx: ToolContext): Promise<string>;
16
50
  };
51
+ export {};
@@ -1,30 +1,148 @@
1
1
  import { z } from 'zod';
2
- import { code, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
2
+ import { code, compactText, formatDateTime, mdEmpty, mdKeyValue, mdLearnedVoice, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
+ import { validateArgs } from '../lib/tool-runtime.js';
3
4
  export const listBrandsTool = {
4
5
  name: 'list_brands',
5
- description: 'List brands/clients the caller can access. Returns each brand ID, name, workspace ID, source, and how many social accounts are currently assigned to it.',
6
+ description: 'Brands (clients) and their saved context. view=brands (default): brands the caller can access with brand ID, name, workspace ID, source, and how many social accounts are assigned (optional workspace_id). view=brand (brand_id): one brand with its workspace, source, and linked legacy brand group. view=accounts (brand_id): the connected social accounts assigned to the brand; use it when a user refers to a brand name rather than an account handle. view=profile (brand_id): voice and tone, audience, keywords, dos and don’ts, visual notes, and other saved brand context.',
6
7
  inputSchema: z.object({
8
+ view: z
9
+ .enum(['brands', 'brand', 'accounts', 'profile'])
10
+ .optional()
11
+ .describe('brands (default), brand, accounts, or profile.'),
12
+ brand_id: z.string().optional().describe('The brand ID (from view=brands). Required for brand, accounts, and profile.'),
7
13
  workspace_id: z
8
14
  .string()
9
15
  .optional()
10
- .describe('Filter to brands in a specific workspace (get IDs via whoami).'),
16
+ .describe('view=brands: filter to brands in a specific workspace (get IDs via whoami).'),
11
17
  }),
12
- async execute(client, input) {
13
- const brands = await client.listBrands({ workspace_id: input.workspace_id });
14
- if (brands.length === 0) {
15
- return input.workspace_id
16
- ? mdEmpty('brands', 'This workspace does not have any brands or clients yet.')
17
- : mdEmpty('brands', 'No brands or clients were found for this API key.', 'Create a brand in the posterly dashboard, then run this again.');
18
- }
19
- return [
20
- mdTitle(`Brands (${brands.length})`),
21
- mdTable(['Brand', 'Brand ID', 'Accounts', 'Source', 'Workspace'], brands.map((brand) => [
22
- brand.name,
23
- code(brand.id),
24
- brand.account_count,
25
- brand.source,
26
- brand.workspace_id ? code(brand.workspace_id) : 'default',
27
- ])),
28
- ].join('\n\n');
18
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
19
+ legacySchemas: {
20
+ get_brand: z.object({ brand_id: z.string() }),
21
+ list_brand_accounts: z.object({ brand_id: z.string() }),
22
+ get_brand_profile: z.object({ brand_id: z.string() }),
23
+ },
24
+ async execute(client, input, ctx) {
25
+ const { mode, args } = validateArgs(ctx, 'list_brands', input);
26
+ if (mode === 'brand')
27
+ return brand(client, args);
28
+ if (mode === 'accounts')
29
+ return brandAccounts(client, args);
30
+ if (mode === 'profile')
31
+ return brandProfile(client, args);
32
+ return brands(client, args);
29
33
  },
30
34
  };
35
+ async function brands(client, input) {
36
+ const brands = await client.listBrands({ workspace_id: input.workspace_id });
37
+ if (brands.length === 0) {
38
+ return input.workspace_id
39
+ ? mdEmpty('brands', 'This workspace does not have any brands or clients yet.')
40
+ : mdEmpty('brands', 'No brands or clients were found for this API key.', 'Create a brand in the posterly dashboard, then run this again.');
41
+ }
42
+ return [
43
+ mdTitle(`Brands (${brands.length})`),
44
+ mdTable(['Brand', 'Brand ID', 'Accounts', 'Source', 'Workspace'], brands.map((brand) => [
45
+ brand.name,
46
+ code(brand.id),
47
+ brand.account_count,
48
+ brand.source,
49
+ brand.workspace_id ? code(brand.workspace_id) : 'default',
50
+ ])),
51
+ ].join('\n\n');
52
+ }
53
+ async function brand(client, input) {
54
+ const result = await client.getBrand(input.brand_id);
55
+ const brand = result.brand;
56
+ return [
57
+ mdTitle(`Brand: ${brand.name}`),
58
+ mdKeyValue([
59
+ ['Brand ID', code(brand.id)],
60
+ ['Source', brand.source],
61
+ ['Workspace', brand.workspace_id ? code(brand.workspace_id) : 'n/a'],
62
+ ['Accounts assigned', brand.account_count ?? 0],
63
+ ['Workspace client ID', brand.workspace_client_id ? code(brand.workspace_client_id) : undefined],
64
+ ['Legacy brand group ID', brand.legacy_brand_group_id ? code(brand.legacy_brand_group_id) : undefined],
65
+ ['Created', formatDateTime(brand.created_at)],
66
+ ['Updated', formatDateTime(brand.updated_at)],
67
+ ]),
68
+ ].join('\n\n');
69
+ }
70
+ async function brandAccounts(client, input) {
71
+ const accounts = await client.listBrandAccounts(input.brand_id);
72
+ if (accounts.length === 0) {
73
+ return mdEmpty('brand accounts', 'No social accounts are currently assigned to this brand.');
74
+ }
75
+ return [
76
+ mdTitle(`Brand accounts (${accounts.length})`),
77
+ mdTable(['Platform', 'Account', 'Account ID', 'Workspace'], accounts.map((account) => [
78
+ account.platform,
79
+ `@${account.username}`,
80
+ code(account.id),
81
+ account.workspace_id ? code(account.workspace_id) : 'default',
82
+ ])),
83
+ ].join('\n\n');
84
+ }
85
+ async function brandProfile(client, input) {
86
+ const result = await client.getBrandProfile(input.brand_id);
87
+ const profile = result.brand_profile;
88
+ const profileBody = [
89
+ mdKeyValue([
90
+ ['Source', profile.source],
91
+ ['Profile ID', profile.id ? code(profile.id) : undefined],
92
+ ['Workspace client ID', profile.workspace_client_id ? code(profile.workspace_client_id) : undefined],
93
+ ['Legacy brand group ID', profile.brand_group_id ? code(profile.brand_group_id) : undefined],
94
+ ['Logo', profile.logo_url],
95
+ ['Last refreshed', profile.last_context_refresh_at ? formatDateTime(profile.last_context_refresh_at) : undefined],
96
+ ]),
97
+ mdSection('Voice and audience', mdKeyValue([
98
+ ['Tone of voice', compactText(profile.tone_of_voice, 180)],
99
+ ['Audience', compactText(profile.audience, 180)],
100
+ ['Brand values', compactText(profile.brand_values, 180)],
101
+ ['Brand story', compactText(formatStructuredValue(profile.brand_story), 240)],
102
+ ])),
103
+ mdSection('Guidance', mdKeyValue([
104
+ ['Custom instructions', compactText(profile.custom_instructions, 240)],
105
+ ['Keywords', formatStructuredValue(profile.keywords)],
106
+ ['Competitors', formatStructuredValue(profile.competitors)],
107
+ ['Do / Don’ts', formatStructuredValue(profile.do_donts)],
108
+ ['Topics to cover', formatStructuredValue(profile.topics_to_cover)],
109
+ ['Topics to avoid', formatStructuredValue(profile.topics_to_avoid)],
110
+ ])),
111
+ mdSection('Creative references', mdKeyValue([
112
+ ['Visual guidelines', formatStructuredValue(profile.visual_guidelines)],
113
+ ['Example posts', formatStructuredValue(profile.example_posts)],
114
+ ['Voice examples', formatStructuredValue(profile.voice_examples)],
115
+ ])),
116
+ ].filter(Boolean).join('\n\n');
117
+ const learnedVoices = (Array.isArray(profile.learned_voice) ? profile.learned_voice : []);
118
+ const learnedBody = learnedVoices.length
119
+ ? mdSection('Learned voice (from published captions)', learnedVoices.map((voice) => mdLearnedVoice(voice)).filter(Boolean).join('\n\n'))
120
+ : '';
121
+ return [
122
+ mdTitle(`Brand profile: ${profile.brand_name}`),
123
+ profileBody || 'No extended brand profile fields have been saved yet.',
124
+ learnedBody ||
125
+ '_No learned voice yet. posterly learns each account’s voice automatically once it has enough published posts._',
126
+ ].filter(Boolean).join('\n\n');
127
+ }
128
+ function formatStructuredValue(value) {
129
+ if (value == null)
130
+ return null;
131
+ if (typeof value === 'string') {
132
+ const trimmed = value.trim();
133
+ return trimmed.length > 0 ? trimmed : null;
134
+ }
135
+ if (Array.isArray(value)) {
136
+ if (value.length === 0)
137
+ return null;
138
+ return value
139
+ .map((item) => formatStructuredValue(item) || JSON.stringify(item))
140
+ .filter(Boolean)
141
+ .join(' | ');
142
+ }
143
+ if (typeof value === 'object') {
144
+ const json = JSON.stringify(value);
145
+ return json === '{}' ? null : json;
146
+ }
147
+ return String(value);
148
+ }