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
@@ -1,9 +1,21 @@
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 CommentsInput = {
5
+ comment_id?: string;
6
+ workspace_id?: string;
7
+ account_id?: number;
8
+ platform?: 'instagram' | 'facebook' | 'threads' | 'linkedin';
9
+ filter?: 'all' | 'unread' | 'flagged';
10
+ search?: string;
11
+ limit?: number;
12
+ offset?: number;
13
+ };
3
14
  export declare const listCommentsTool: {
4
15
  name: string;
5
16
  description: string;
6
17
  inputSchema: z.ZodObject<{
18
+ comment_id: z.ZodOptional<z.ZodString>;
7
19
  workspace_id: z.ZodOptional<z.ZodString>;
8
20
  account_id: z.ZodOptional<z.ZodNumber>;
9
21
  platform: z.ZodOptional<z.ZodEnum<["instagram", "facebook", "threads", "linkedin"]>>;
@@ -19,6 +31,7 @@ export declare const listCommentsTool: {
19
31
  offset?: number | undefined;
20
32
  filter?: "all" | "unread" | "flagged" | undefined;
21
33
  search?: string | undefined;
34
+ comment_id?: string | undefined;
22
35
  }, {
23
36
  platform?: "instagram" | "facebook" | "threads" | "linkedin" | undefined;
24
37
  workspace_id?: string | undefined;
@@ -27,14 +40,21 @@ export declare const listCommentsTool: {
27
40
  offset?: number | undefined;
28
41
  filter?: "all" | "unread" | "flagged" | undefined;
29
42
  search?: string | undefined;
43
+ comment_id?: string | undefined;
30
44
  }>;
31
- execute(client: PosterlyClient, input: {
32
- workspace_id?: string;
33
- account_id?: number;
34
- platform?: "instagram" | "facebook" | "threads" | "linkedin";
35
- filter?: "all" | "unread" | "flagged";
36
- search?: string;
37
- limit?: number;
38
- offset?: number;
39
- }): Promise<string>;
45
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
46
+ legacySchemas: {
47
+ get_comment: z.ZodObject<{
48
+ comment_id: z.ZodString;
49
+ workspace_id: z.ZodOptional<z.ZodString>;
50
+ }, "strip", z.ZodTypeAny, {
51
+ comment_id: string;
52
+ workspace_id?: string | undefined;
53
+ }, {
54
+ comment_id: string;
55
+ workspace_id?: string | undefined;
56
+ }>;
57
+ };
58
+ execute(client: PosterlyClient, input: CommentsInput, ctx: ToolContext): Promise<string>;
40
59
  };
60
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { truncateText } from '../lib/format.js';
2
+ import { formatLocalDateTime, truncateText } from '../lib/format.js';
3
+ import { validateArgs } from '../lib/tool-runtime.js';
3
4
  const INBOX_PLATFORMS = ['instagram', 'facebook', 'threads', 'linkedin'];
4
5
  /**
5
6
  * The shared `InboxConversationsResponse`/`InboxCommentsResponse` types
@@ -20,8 +21,9 @@ function describeSkippedWorkspaces(result) {
20
21
  }
21
22
  export const listCommentsTool = {
22
23
  name: 'list_comments',
23
- description: 'List social inbox comments and replies for Instagram, Facebook Pages, Threads, and LinkedIn Pages. Filter by account, platform, unread/flagged, or search. Requires a Pro plan or higher plus posts:read.',
24
+ description: 'Social inbox comments and replies for Instagram, Facebook Pages, Threads, and LinkedIn Pages (Pro plan or higher plus posts:read). Without comment_id: list comments filtered by account, platform, unread/flagged, or search, with limit/offset paging. With comment_id: one comment and its replies; only workspace_id may be added.',
24
25
  inputSchema: z.object({
26
+ comment_id: z.string().min(1).optional().describe('One comment (from a list call) with its replies.'),
25
27
  workspace_id: z.string().optional().describe('Optional. Omit to search every workspace you belong to; pass a workspace ID from whoami to limit to one.'),
26
28
  account_id: z.number().int().positive().optional().describe('Social account ID from list_accounts.'),
27
29
  platform: z.enum(INBOX_PLATFORMS).optional().describe('instagram, facebook, threads, or linkedin.'),
@@ -30,37 +32,76 @@ export const listCommentsTool = {
30
32
  limit: z.number().int().min(1).max(100).optional().describe('Number of comments to return. Default 50, max 100.'),
31
33
  offset: z.number().int().min(0).optional().describe('Skip this many comments before returning results.'),
32
34
  }),
33
- async execute(client, input) {
34
- const result = await client.listComments(input);
35
- const skippedNote = describeSkippedWorkspaces(result);
36
- if (result.comments.length === 0) {
37
- return [
38
- 'No inbox comments found matching your filters.',
39
- skippedNote,
40
- ].filter(Boolean).join('\n\n');
41
- }
42
- const lines = [`Inbox comments (showing ${result.comments.length} of ${result.total}):`];
43
- for (const comment of result.comments) {
44
- const author = typeof comment.author_username === 'string' ? `@${comment.author_username}` : 'unknown';
45
- const platform = typeof comment.platform === 'string' ? comment.platform : 'unknown';
46
- const flags = [];
47
- if (comment.is_read === false)
48
- flags.push('unread');
49
- if (comment.is_hidden)
50
- flags.push('hidden');
51
- if (comment.is_flagged)
52
- flags.push('flagged');
53
- const flagLabel = flags.length > 0 ? `, ${flags.join(', ')}` : '';
54
- const text = truncateText(typeof comment.content === 'string' ? comment.content : '', 80);
55
- const replies = Array.isArray(comment.replies) ? comment.replies.length : 0;
56
- lines.push(`- ${String(comment.id)} (${platform}, ${author}${flagLabel}${replies ? `, ${replies} replies` : ''}): ${text}`);
57
- }
58
- if (result.hasMore) {
59
- const nextOffset = (input.offset ?? 0) + result.comments.length;
60
- lines.push('', `More comments available. Call again with offset: ${nextOffset}.`);
61
- }
62
- if (skippedNote)
63
- lines.push('', skippedNote);
64
- return lines.join('\n');
35
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
36
+ legacySchemas: {
37
+ get_comment: z.object({
38
+ comment_id: z.string().min(1),
39
+ workspace_id: z.string().optional(),
40
+ }),
41
+ },
42
+ async execute(client, input, ctx) {
43
+ const { mode, args } = validateArgs(ctx, 'list_comments', input);
44
+ if (mode === 'comment')
45
+ return oneComment(client, args);
46
+ return commentList(client, args);
65
47
  },
66
48
  };
49
+ async function commentList(client, input) {
50
+ const result = await client.listComments(input);
51
+ const skippedNote = describeSkippedWorkspaces(result);
52
+ if (result.comments.length === 0) {
53
+ return [
54
+ 'No inbox comments found matching your filters.',
55
+ skippedNote,
56
+ ].filter(Boolean).join('\n\n');
57
+ }
58
+ const lines = [`Inbox comments (showing ${result.comments.length} of ${result.total}):`];
59
+ for (const comment of result.comments) {
60
+ const author = typeof comment.author_username === 'string' ? `@${comment.author_username}` : 'unknown';
61
+ const platform = typeof comment.platform === 'string' ? comment.platform : 'unknown';
62
+ const flags = [];
63
+ if (comment.is_read === false)
64
+ flags.push('unread');
65
+ if (comment.is_hidden)
66
+ flags.push('hidden');
67
+ if (comment.is_flagged)
68
+ flags.push('flagged');
69
+ const flagLabel = flags.length > 0 ? `, ${flags.join(', ')}` : '';
70
+ const text = truncateText(typeof comment.content === 'string' ? comment.content : '', 80);
71
+ const replies = Array.isArray(comment.replies) ? comment.replies.length : 0;
72
+ lines.push(`- ${String(comment.id)} (${platform}, ${author}${flagLabel}${replies ? `, ${replies} replies` : ''}): ${text}`);
73
+ }
74
+ if (result.hasMore) {
75
+ const nextOffset = (input.offset ?? 0) + result.comments.length;
76
+ lines.push('', `More comments available. Call again with offset: ${nextOffset}.`);
77
+ }
78
+ if (skippedNote)
79
+ lines.push('', skippedNote);
80
+ return lines.join('\n');
81
+ }
82
+ async function oneComment(client, input) {
83
+ const result = await client.getComment(input);
84
+ const comment = result.comment;
85
+ const author = typeof comment.author_username === 'string' ? `@${comment.author_username}` : 'unknown';
86
+ const lines = [
87
+ `Comment ${String(comment.id)}`,
88
+ `• Platform: ${typeof comment.platform === 'string' ? comment.platform : 'unknown'}`,
89
+ `• Author: ${author}`,
90
+ `• Hidden: ${comment.is_hidden ? 'yes' : 'no'}`,
91
+ `• Read: ${comment.is_read === false ? 'no' : 'yes'}`,
92
+ `• Posted: ${formatLocalDateTime(typeof comment.posted_at === 'string' ? comment.posted_at : null)}`,
93
+ `• Content: ${typeof comment.content === 'string' ? comment.content : '(empty)'}`,
94
+ ];
95
+ if (typeof comment.media_permalink === 'string')
96
+ lines.push(`• Media: ${comment.media_permalink}`);
97
+ const replies = Array.isArray(comment.replies) ? comment.replies : [];
98
+ if (replies.length > 0) {
99
+ lines.push('', `Replies (${replies.length}):`);
100
+ for (const reply of replies) {
101
+ const row = reply;
102
+ const replyAuthor = typeof row.author_username === 'string' ? `@${row.author_username}` : 'unknown';
103
+ lines.push(`- ${String(row.id)} ${replyAuthor}: ${typeof row.content === 'string' ? row.content : '(empty)'}`);
104
+ }
105
+ }
106
+ return lines.join('\n');
107
+ }
@@ -1,9 +1,21 @@
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 ConversationsInput = {
5
+ conversation_id?: string;
6
+ workspace_id?: string;
7
+ account_id?: number;
8
+ platform?: 'instagram' | 'facebook' | 'threads';
9
+ filter?: 'all' | 'unread' | 'starred' | 'archived';
10
+ search?: string;
11
+ limit?: number;
12
+ offset?: number;
13
+ };
3
14
  export declare const listConversationsTool: {
4
15
  name: string;
5
16
  description: string;
6
17
  inputSchema: z.ZodObject<{
18
+ conversation_id: z.ZodOptional<z.ZodString>;
7
19
  workspace_id: z.ZodOptional<z.ZodString>;
8
20
  account_id: z.ZodOptional<z.ZodNumber>;
9
21
  platform: z.ZodOptional<z.ZodEnum<["instagram", "facebook", "threads"]>>;
@@ -19,6 +31,7 @@ export declare const listConversationsTool: {
19
31
  offset?: number | undefined;
20
32
  filter?: "all" | "unread" | "starred" | "archived" | undefined;
21
33
  search?: string | undefined;
34
+ conversation_id?: string | undefined;
22
35
  }, {
23
36
  platform?: "instagram" | "facebook" | "threads" | undefined;
24
37
  workspace_id?: string | undefined;
@@ -27,14 +40,24 @@ export declare const listConversationsTool: {
27
40
  offset?: number | undefined;
28
41
  filter?: "all" | "unread" | "starred" | "archived" | undefined;
29
42
  search?: string | undefined;
43
+ conversation_id?: string | undefined;
30
44
  }>;
31
- execute(client: PosterlyClient, input: {
32
- workspace_id?: string;
33
- account_id?: number;
34
- platform?: "instagram" | "facebook" | "threads";
35
- filter?: "all" | "unread" | "starred" | "archived";
36
- search?: string;
37
- limit?: number;
38
- offset?: number;
39
- }): Promise<string>;
45
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
46
+ legacySchemas: {
47
+ get_conversation: z.ZodObject<{
48
+ conversation_id: z.ZodString;
49
+ workspace_id: z.ZodOptional<z.ZodString>;
50
+ limit: z.ZodOptional<z.ZodNumber>;
51
+ }, "strip", z.ZodTypeAny, {
52
+ conversation_id: string;
53
+ workspace_id?: string | undefined;
54
+ limit?: number | undefined;
55
+ }, {
56
+ conversation_id: string;
57
+ workspace_id?: string | undefined;
58
+ limit?: number | undefined;
59
+ }>;
60
+ };
61
+ execute(client: PosterlyClient, input: ConversationsInput, ctx: ToolContext): Promise<string>;
40
62
  };
63
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { truncateText } from '../lib/format.js';
2
+ import { formatLocalDateTime, truncateText } from '../lib/format.js';
3
+ import { validateArgs } from '../lib/tool-runtime.js';
3
4
  const INBOX_PLATFORMS = ['instagram', 'facebook', 'threads'];
4
5
  /**
5
6
  * The shared `InboxConversationsResponse`/`InboxCommentsResponse` types
@@ -20,39 +21,74 @@ function describeSkippedWorkspaces(result) {
20
21
  }
21
22
  export const listConversationsTool = {
22
23
  name: 'list_conversations',
23
- description: 'List social inbox DM conversations for Instagram and Facebook Page inboxes. Filter by account, platform, unread/starred/archived, or search. Requires a Pro plan or higher plus posts:read. Threads has no DMs.',
24
+ description: 'Social inbox DM conversations for Instagram and Facebook Page inboxes (Threads has no DMs; Pro plan or higher plus posts:read). Without conversation_id: list conversations filtered by account, platform, unread/starred/archived, or search, with limit/offset paging. With conversation_id: one conversation and its messages; there limit is the number of messages (default 50, max 100) and only workspace_id may be added.',
24
25
  inputSchema: z.object({
26
+ conversation_id: z.string().min(1).optional().describe('One conversation (from a list call) with its messages.'),
25
27
  workspace_id: z.string().optional().describe('Optional. Omit to search every workspace you belong to; pass a workspace ID from whoami to limit to one.'),
26
28
  account_id: z.number().int().positive().optional().describe('Social account ID from list_accounts.'),
27
29
  platform: z.enum(INBOX_PLATFORMS).optional().describe('instagram or facebook. Threads has no DMs.'),
28
30
  filter: z.enum(['all', 'unread', 'starred', 'archived']).optional().describe('Conversation filter. Defaults to all (excludes archived).'),
29
31
  search: z.string().optional().describe('Search participant username or last message preview.'),
30
- limit: z.number().int().min(1).max(100).optional().describe('Number of conversations to return. Default 50, max 100.'),
32
+ limit: z.number().int().min(1).max(100).optional().describe('Conversations to return, or messages with conversation_id. Default 50, max 100.'),
31
33
  offset: z.number().int().min(0).optional().describe('Skip this many conversations before returning results.'),
32
34
  }),
33
- async execute(client, input) {
34
- const result = await client.listConversations(input);
35
- const skippedNote = describeSkippedWorkspaces(result);
36
- if (result.conversations.length === 0) {
37
- return [
38
- 'No inbox conversations found matching your filters.',
39
- skippedNote,
40
- ].filter(Boolean).join('\n\n');
41
- }
42
- const lines = [`Inbox conversations (showing ${result.conversations.length} of ${result.total}):`];
43
- for (const conversation of result.conversations) {
44
- const username = typeof conversation.participant_username === 'string' ? `@${conversation.participant_username}` : 'unknown';
45
- const platform = typeof conversation.platform === 'string' ? conversation.platform : 'unknown';
46
- const unread = conversation.is_read === false ? 'unread' : 'read';
47
- const preview = truncateText(typeof conversation.last_message_preview === 'string' ? conversation.last_message_preview : '', 80);
48
- lines.push(`- ${String(conversation.id)} (${platform}, ${username}, ${unread}): ${preview}`);
49
- }
50
- if (result.hasMore) {
51
- const nextOffset = (input.offset ?? 0) + result.conversations.length;
52
- lines.push('', `More conversations available. Call again with offset: ${nextOffset}.`);
53
- }
54
- if (skippedNote)
55
- lines.push('', skippedNote);
56
- return lines.join('\n');
35
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
36
+ legacySchemas: {
37
+ get_conversation: z.object({
38
+ conversation_id: z.string().min(1),
39
+ workspace_id: z.string().optional(),
40
+ limit: z.number().int().min(1).max(100).optional(),
41
+ }),
42
+ },
43
+ async execute(client, input, ctx) {
44
+ const { mode, args } = validateArgs(ctx, 'list_conversations', input);
45
+ if (mode === 'conversation')
46
+ return oneConversation(client, args);
47
+ return conversationList(client, args);
57
48
  },
58
49
  };
50
+ async function conversationList(client, input) {
51
+ const result = await client.listConversations(input);
52
+ const skippedNote = describeSkippedWorkspaces(result);
53
+ if (result.conversations.length === 0) {
54
+ return [
55
+ 'No inbox conversations found matching your filters.',
56
+ skippedNote,
57
+ ].filter(Boolean).join('\n\n');
58
+ }
59
+ const lines = [`Inbox conversations (showing ${result.conversations.length} of ${result.total}):`];
60
+ for (const conversation of result.conversations) {
61
+ const username = typeof conversation.participant_username === 'string' ? `@${conversation.participant_username}` : 'unknown';
62
+ const platform = typeof conversation.platform === 'string' ? conversation.platform : 'unknown';
63
+ const unread = conversation.is_read === false ? 'unread' : 'read';
64
+ const preview = truncateText(typeof conversation.last_message_preview === 'string' ? conversation.last_message_preview : '', 80);
65
+ lines.push(`- ${String(conversation.id)} (${platform}, ${username}, ${unread}): ${preview}`);
66
+ }
67
+ if (result.hasMore) {
68
+ const nextOffset = (input.offset ?? 0) + result.conversations.length;
69
+ lines.push('', `More conversations available. Call again with offset: ${nextOffset}.`);
70
+ }
71
+ if (skippedNote)
72
+ lines.push('', skippedNote);
73
+ return lines.join('\n');
74
+ }
75
+ async function oneConversation(client, input) {
76
+ const result = await client.getConversation(input);
77
+ const conversation = result.conversation;
78
+ const username = typeof conversation.participant_username === 'string' ? `@${conversation.participant_username}` : 'unknown';
79
+ const lines = [
80
+ `Conversation ${String(conversation.id)}`,
81
+ `• Platform: ${typeof conversation.platform === 'string' ? conversation.platform : 'unknown'}`,
82
+ `• Participant: ${username}`,
83
+ `• Read: ${conversation.is_read === false ? 'no' : 'yes'}`,
84
+ `• Messages: ${result.messages.length}`,
85
+ '',
86
+ ];
87
+ for (const message of result.messages) {
88
+ const who = message.sender_is_business ? 'you' : (typeof message.sender_username === 'string' ? message.sender_username : 'them');
89
+ const when = formatLocalDateTime(typeof message.sent_at === 'string' ? message.sent_at : null);
90
+ const text = truncateText(typeof message.content === 'string' ? message.content : '', 240);
91
+ lines.push(`- [${when}] ${who}: ${text}`);
92
+ }
93
+ return lines.join('\n');
94
+ }
@@ -5,7 +5,7 @@ export const listGoogleBusinessMediaTool = {
5
5
  inputSchema: z.object({
6
6
  workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
7
7
  account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
8
- location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
8
+ location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID from list_google_business_reviews view=review_link. Prefer account_id.'),
9
9
  }),
10
10
  async execute(client, input) {
11
11
  const result = await client.listGoogleBusinessMedia(input);
@@ -1,9 +1,21 @@
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 ReviewsInput = {
5
+ view?: 'reviews' | 'review_link';
6
+ workspace_id?: string;
7
+ account_id?: number;
8
+ location_id?: string;
9
+ rating?: number;
10
+ unanswered?: boolean;
11
+ limit?: number;
12
+ offset?: number;
13
+ };
3
14
  export declare const listGoogleBusinessReviewsTool: {
4
15
  name: string;
5
16
  description: string;
6
17
  inputSchema: z.ZodObject<{
18
+ view: z.ZodOptional<z.ZodEnum<["reviews", "review_link"]>>;
7
19
  workspace_id: z.ZodOptional<z.ZodString>;
8
20
  account_id: z.ZodOptional<z.ZodNumber>;
9
21
  location_id: z.ZodOptional<z.ZodString>;
@@ -19,6 +31,7 @@ export declare const listGoogleBusinessReviewsTool: {
19
31
  location_id?: string | undefined;
20
32
  rating?: number | undefined;
21
33
  unanswered?: boolean | undefined;
34
+ view?: "reviews" | "review_link" | undefined;
22
35
  }, {
23
36
  workspace_id?: string | undefined;
24
37
  account_id?: number | undefined;
@@ -27,14 +40,24 @@ export declare const listGoogleBusinessReviewsTool: {
27
40
  location_id?: string | undefined;
28
41
  rating?: number | undefined;
29
42
  unanswered?: boolean | undefined;
43
+ view?: "reviews" | "review_link" | undefined;
30
44
  }>;
31
- execute(client: PosterlyClient, input: {
32
- workspace_id?: string;
33
- account_id?: number;
34
- location_id?: string;
35
- rating?: number;
36
- unanswered?: boolean;
37
- limit?: number;
38
- offset?: number;
39
- }): Promise<string>;
45
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
46
+ legacySchemas: {
47
+ get_google_business_review_link: z.ZodObject<{
48
+ workspace_id: z.ZodOptional<z.ZodString>;
49
+ account_id: z.ZodOptional<z.ZodNumber>;
50
+ location_id: z.ZodOptional<z.ZodString>;
51
+ }, "strip", z.ZodTypeAny, {
52
+ workspace_id?: string | undefined;
53
+ account_id?: number | undefined;
54
+ location_id?: string | undefined;
55
+ }, {
56
+ workspace_id?: string | undefined;
57
+ account_id?: number | undefined;
58
+ location_id?: string | undefined;
59
+ }>;
60
+ };
61
+ execute(client: PosterlyClient, input: ReviewsInput, ctx: ToolContext): Promise<string>;
40
62
  };
63
+ export {};
@@ -1,79 +1,104 @@
1
1
  import { z } from 'zod';
2
2
  import { formatDate } from '../lib/format.js';
3
+ import { validateArgs } from '../lib/tool-runtime.js';
3
4
  /** Matches GOOGLE_BUSINESS_REVIEWS_MAX_LIMIT in lib/api-v1-google-business.ts. */
4
5
  const MAX_LIMIT = 1000;
5
6
  export const listGoogleBusinessReviewsTool = {
6
7
  name: 'list_google_business_reviews',
7
- description: 'List Google Business Profile reviews for one location/account or every accessible GBP location. Pages through all reviews Google has (not just the first 50), returns the full untruncated review text with its date, and reports each location\'s total review count. Supports rating and unanswered-only filters plus limit/offset paging.',
8
+ description: 'Google Business Profile reviews. view=reviews (default): reviews for one location/account or every accessible GBP location, paging through all reviews Google has (not just the first 50) with the full untruncated review text, its date, and each location\'s total review count; filter by rating or unanswered, page with limit/offset. view=review_link (account_id or location_id): the direct public Google review link plus the Google Place ID (ChIJ...); that Place ID is output only and must NOT be passed back as location_id.',
8
9
  inputSchema: z.object({
10
+ view: z.enum(['reviews', 'review_link']).optional().describe('reviews (default) or review_link.'),
9
11
  workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
10
12
  account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
11
- location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
12
- rating: z.number().int().min(1).max(5).optional().describe('Filter to a 1-5 star rating.'),
13
- unanswered: z.boolean().optional().describe('Only return reviews without owner replies.'),
13
+ location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID from view=review_link. Prefer account_id.'),
14
+ rating: z.number().int().min(1).max(5).optional().describe('reviews: filter to a 1-5 star rating.'),
15
+ unanswered: z.boolean().optional().describe('reviews: only return reviews without owner replies.'),
14
16
  limit: z
15
17
  .number()
16
18
  .int()
17
19
  .min(1)
18
20
  .max(MAX_LIMIT)
19
21
  .optional()
20
- .describe(`Maximum reviews to return in this call (default 200, max ${MAX_LIMIT}). Every returned review is listed in full.`),
22
+ .describe(`reviews: maximum reviews to return in this call (default 200, max ${MAX_LIMIT}). Every returned review is listed in full.`),
21
23
  offset: z
22
24
  .number()
23
25
  .int()
24
26
  .min(0)
25
27
  .optional()
26
- .describe('Skip this many reviews before returning results. Use with limit to page through a location with more reviews than one call returns.'),
28
+ .describe('reviews: skip this many reviews before returning results. Use with limit to page through a location with more reviews than one call returns.'),
27
29
  }),
28
- async execute(client, input) {
29
- const result = await client.listGoogleBusinessReviews(input);
30
- const locations = (result.locations || []);
31
- const offset = result.offset ?? 0;
32
- if (result.reviews.length === 0) {
33
- // Distinguish "no reviews exist" from "we couldn't read them" — a silent
34
- // zero used to be indistinguishable from a broken connection.
35
- const lines = ['No Google Business reviews found matching your filters.'];
36
- const locationLines = locations.map((location) => formatLocationLine(location));
37
- if (locationLines.length > 0) {
38
- lines.push('', 'Locations checked:', ...locationLines);
39
- }
40
- return lines.join('\n');
41
- }
42
- const header = [`Google Business reviews (showing ${result.reviews.length} of ${result.total} matching`];
43
- if (offset > 0)
44
- header.push(`, from offset ${offset}`);
45
- if (typeof result.total_review_count === 'number') {
46
- header.push(`; ${result.total_review_count} total on Google`);
47
- }
48
- header.push('):');
49
- const lines = [header.join('')];
50
- for (const location of locations) {
51
- if (location.error) {
52
- lines.push(`! ${location.business_name || location.name || location.location_id || 'location'}: ${location.error}`);
53
- }
30
+ /** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
31
+ legacySchemas: {
32
+ get_google_business_review_link: z.object({
33
+ workspace_id: z.string().optional(),
34
+ account_id: z.number().int().positive().optional(),
35
+ location_id: z.string().optional(),
36
+ }),
37
+ },
38
+ async execute(client, input, ctx) {
39
+ const { mode, args } = validateArgs(ctx, 'list_google_business_reviews', input);
40
+ if (mode === 'review_link')
41
+ return reviewLink(client, args);
42
+ return reviews(client, args);
43
+ },
44
+ };
45
+ async function reviews(client, input) {
46
+ const result = await client.listGoogleBusinessReviews(input);
47
+ const locations = (result.locations || []);
48
+ const offset = result.offset ?? 0;
49
+ if (result.reviews.length === 0) {
50
+ // Distinguish "no reviews exist" from "we couldn't read them": a silent
51
+ // zero used to be indistinguishable from a broken connection.
52
+ const lines = ['No Google Business reviews found matching your filters.'];
53
+ const locationLines = locations.map((location) => formatLocationLine(location));
54
+ if (locationLines.length > 0) {
55
+ lines.push('', 'Locations checked:', ...locationLines);
54
56
  }
55
- lines.push('');
56
- for (const review of result.reviews) {
57
- const name = review.reviewer?.displayName || 'Anonymous';
58
- const rating = review.starRating ? `${review.starRating} stars` : 'unrated';
59
- const location = review.businessName || review.locationName || review.locationId || 'location';
60
- const date = formatReviewDate(review);
61
- const replied = review.reviewReply ? 'replied' : 'unanswered';
62
- lines.push(`- ${name} (${rating}, ${date}, ${location}, ${replied})`);
63
- // Full review text, no truncation — collapse whitespace only so the
64
- // bullet list stays readable for multi-paragraph reviews.
65
- if (review.comment) {
66
- lines.push(` ${review.comment.replace(/\s+/g, ' ').trim()}`);
67
- }
57
+ return lines.join('\n');
58
+ }
59
+ const header = [`Google Business reviews (showing ${result.reviews.length} of ${result.total} matching`];
60
+ if (offset > 0)
61
+ header.push(`, from offset ${offset}`);
62
+ if (typeof result.total_review_count === 'number') {
63
+ header.push(`; ${result.total_review_count} total on Google`);
64
+ }
65
+ header.push('):');
66
+ const lines = [header.join('')];
67
+ for (const location of locations) {
68
+ if (location.error) {
69
+ lines.push(`! ${location.business_name || location.name || location.location_id || 'location'}: ${location.error}`);
68
70
  }
69
- if (result.has_more) {
70
- const nextOffset = offset + result.reviews.length;
71
- lines.push('', `More reviews available — call again with offset: ${nextOffset} to continue.`);
71
+ }
72
+ lines.push('');
73
+ for (const review of result.reviews) {
74
+ const name = review.reviewer?.displayName || 'Anonymous';
75
+ const rating = review.starRating ? `${review.starRating} stars` : 'unrated';
76
+ const location = review.businessName || review.locationName || review.locationId || 'location';
77
+ const date = formatReviewDate(review);
78
+ const replied = review.reviewReply ? 'replied' : 'unanswered';
79
+ lines.push(`- ${name} (${rating}, ${date}, ${location}, ${replied})`);
80
+ // Full review text, no truncation; collapse whitespace only so the
81
+ // bullet list stays readable for multi-paragraph reviews.
82
+ if (review.comment) {
83
+ lines.push(` ${review.comment.replace(/\s+/g, ' ').trim()}`);
72
84
  }
73
- lines.push('', 'Locations:', ...locations.map((location) => formatLocationLine(location)));
74
- return lines.join('\n');
75
- },
76
- };
85
+ }
86
+ if (result.has_more) {
87
+ const nextOffset = offset + result.reviews.length;
88
+ lines.push('', `More reviews available. Call again with offset: ${nextOffset} to continue.`);
89
+ }
90
+ lines.push('', 'Locations:', ...locations.map((location) => formatLocationLine(location)));
91
+ return lines.join('\n');
92
+ }
93
+ async function reviewLink(client, input) {
94
+ const result = await client.getGoogleBusinessReviewLink(input);
95
+ return [
96
+ `Review link for ${result.businessName}:`,
97
+ result.reviewLink,
98
+ `Place ID: ${result.placeId}`,
99
+ `Account ID: ${result.account_id}`,
100
+ ].join('\n');
101
+ }
77
102
  /**
78
103
  * When a review was written, plus when it was last edited if that differs.
79
104
  * The list is ordered by last activity (updateTime), so showing only the