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.
- package/README.md +310 -111
- package/dist/index.js +190 -894
- package/dist/lib/alias-transport.d.ts +84 -0
- package/dist/lib/alias-transport.js +193 -0
- package/dist/lib/api-client.d.ts +18 -0
- package/dist/lib/api-client.js +55 -0
- package/dist/lib/preview-token-core.d.ts +62 -0
- package/dist/lib/preview-token-core.js +131 -0
- package/dist/lib/preview-token.d.ts +23 -0
- package/dist/lib/preview-token.js +44 -0
- package/dist/lib/tool-registry.d.ts +246 -0
- package/dist/lib/tool-registry.js +394 -0
- package/dist/lib/tool-runtime.d.ts +68 -0
- package/dist/lib/tool-runtime.js +85 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/audit-google-business-profile.js +1 -1
- package/dist/tools/connect-account.d.ts +4 -4
- package/dist/tools/connect-account.js +2 -2
- package/dist/tools/create-connect-session.d.ts +2 -2
- package/dist/tools/create-connect-session.js +2 -2
- package/dist/tools/create-post.d.ts +6 -6
- package/dist/tools/create-post.js +3 -3
- package/dist/tools/create-posts-batch.d.ts +4 -4
- package/dist/tools/create-webhook.d.ts +2 -2
- package/dist/tools/delete-post.d.ts +34 -10
- package/dist/tools/delete-post.js +52 -16
- package/dist/tools/generate-captions.d.ts +2 -2
- package/dist/tools/generate-image.d.ts +4 -4
- package/dist/tools/generate-image.js +2 -2
- package/dist/tools/generate-video.d.ts +6 -6
- package/dist/tools/generate-video.js +1 -1
- package/dist/tools/get-agent-signup-info.js +2 -2
- package/dist/tools/get-post.d.ts +16 -1
- package/dist/tools/get-post.js +35 -19
- package/dist/tools/list-accounts.d.ts +49 -3
- package/dist/tools/list-accounts.js +148 -33
- package/dist/tools/list-analytics.d.ts +111 -0
- package/dist/tools/list-analytics.js +393 -0
- package/dist/tools/list-brands.d.ts +38 -3
- package/dist/tools/list-brands.js +138 -20
- package/dist/tools/list-comments.d.ts +29 -9
- package/dist/tools/list-comments.js +75 -34
- package/dist/tools/list-conversations.d.ts +32 -9
- package/dist/tools/list-conversations.js +63 -27
- package/dist/tools/list-google-business-media.js +1 -1
- package/dist/tools/list-google-business-reviews.d.ts +32 -9
- package/dist/tools/list-google-business-reviews.js +77 -52
- package/dist/tools/list-jobs.d.ts +60 -0
- package/dist/tools/list-jobs.js +81 -0
- package/dist/tools/list-platforms.d.ts +62 -3
- package/dist/tools/list-platforms.js +118 -9
- package/dist/tools/list-posts.d.ts +2 -2
- package/dist/tools/manage-comment.d.ts +102 -0
- package/dist/tools/manage-comment.js +125 -0
- package/dist/tools/manage-conversation.d.ts +79 -0
- package/dist/tools/manage-conversation.js +75 -0
- package/dist/tools/manage-google-business-media.d.ts +103 -0
- package/dist/tools/manage-google-business-media.js +126 -0
- package/dist/tools/manage-google-business-review.d.ts +146 -0
- package/dist/tools/manage-google-business-review.js +115 -0
- package/dist/tools/manage-oauth-client.d.ts +121 -0
- package/dist/tools/manage-oauth-client.js +94 -0
- package/dist/tools/manage-webhook.d.ts +99 -0
- package/dist/tools/manage-webhook.js +87 -0
- package/dist/tools/start-signup.d.ts +2 -2
- package/dist/tools/submit-agent-feedback.d.ts +4 -4
- package/dist/tools/trigger-platform-helper.d.ts +2 -2
- package/dist/tools/update-post.d.ts +62 -11
- package/dist/tools/update-post.js +79 -26
- package/dist/tools/upload-media.d.ts +32 -9
- package/dist/tools/upload-media.js +54 -18
- package/dist/tools/validate-post.d.ts +4 -4
- package/dist/tools/whoami.d.ts +15 -2
- package/dist/tools/whoami.js +221 -25
- package/package.json +5 -3
- package/scripts/test-alias-transport.mjs +368 -0
- package/scripts/test-client-header-sanitization.mjs +78 -0
- package/scripts/test-tool-annotations.mjs +38 -17
- package/server.json +9 -2
- package/src/index.ts +227 -1443
- package/src/lib/alias-transport.ts +233 -0
- package/src/lib/api-client.ts +65 -0
- package/src/lib/preview-token-core.ts +157 -0
- package/src/lib/preview-token.ts +54 -0
- package/src/lib/tool-registry.ts +538 -0
- package/src/lib/tool-runtime.ts +117 -0
- package/src/lib/version.ts +1 -1
- package/src/tools/audit-google-business-profile.ts +1 -1
- package/src/tools/connect-account.ts +2 -2
- package/src/tools/create-connect-session.ts +2 -2
- package/src/tools/create-post.ts +3 -3
- package/src/tools/delete-post.ts +60 -19
- package/src/tools/generate-image.ts +2 -2
- package/src/tools/generate-video.ts +1 -1
- package/src/tools/get-agent-signup-info.ts +2 -2
- package/src/tools/get-post.ts +35 -21
- package/src/tools/list-accounts.ts +187 -39
- package/src/tools/list-analytics.ts +444 -0
- package/src/tools/list-brands.ts +162 -26
- package/src/tools/list-comments.ts +86 -43
- package/src/tools/list-conversations.ts +81 -39
- package/src/tools/list-google-business-media.ts +1 -1
- package/src/tools/list-google-business-reviews.ts +93 -64
- package/src/tools/list-jobs.ts +91 -0
- package/src/tools/list-platforms.ts +135 -11
- package/src/tools/manage-comment.ts +142 -0
- package/src/tools/manage-conversation.ts +89 -0
- package/src/tools/manage-google-business-media.ts +147 -0
- package/src/tools/manage-google-business-review.ts +139 -0
- package/src/tools/manage-oauth-client.ts +116 -0
- package/src/tools/manage-webhook.ts +103 -0
- package/src/tools/update-post.ts +106 -39
- package/src/tools/upload-media.ts +76 -28
- package/src/tools/whoami.ts +253 -30
- package/tool-annotations.json +76 -149
- package/tool-registry.json +2085 -0
- package/src/tools/add-google-business-media.ts +0 -50
- package/src/tools/create-oauth-client.ts +0 -36
- package/src/tools/delete-comment.ts +0 -29
- package/src/tools/delete-google-business-media.ts +0 -30
- package/src/tools/delete-google-business-review-reply.ts +0 -30
- package/src/tools/delete-oauth-client.ts +0 -23
- package/src/tools/delete-post-group.ts +0 -17
- package/src/tools/delete-webhook.ts +0 -17
- package/src/tools/get-account-analytics.ts +0 -171
- package/src/tools/get-brand-profile.ts +0 -86
- package/src/tools/get-brand.ts +0 -34
- package/src/tools/get-comment.ts +0 -47
- package/src/tools/get-connect-link.ts +0 -61
- package/src/tools/get-connect-session.ts +0 -63
- package/src/tools/get-conversation.ts +0 -44
- package/src/tools/get-google-business-review-link.ts +0 -30
- package/src/tools/get-image-job.ts +0 -36
- package/src/tools/get-learned-voice.ts +0 -31
- package/src/tools/get-mcp-status.ts +0 -212
- package/src/tools/get-performance-profile.ts +0 -47
- package/src/tools/get-platform-schema.ts +0 -56
- package/src/tools/get-post-analytics.ts +0 -243
- package/src/tools/get-post-insights.ts +0 -60
- package/src/tools/get-post-missing.ts +0 -16
- package/src/tools/get-video-job.ts +0 -36
- package/src/tools/list-brand-accounts.ts +0 -36
- package/src/tools/reply-google-business-review.ts +0 -32
- package/src/tools/reply-to-comment.ts +0 -30
- package/src/tools/send-message.ts +0 -28
- package/src/tools/suggest-google-business-review-reply.ts +0 -44
- package/src/tools/sync-inbox.ts +0 -28
- package/src/tools/test-webhook.ts +0 -18
- package/src/tools/update-comment.ts +0 -31
- package/src/tools/update-oauth-client.ts +0 -38
- package/src/tools/update-post-release-id.ts +0 -25
- package/src/tools/update-post-status.ts +0 -31
- package/src/tools/update-webhook.ts +0 -36
- package/src/tools/upload-media-from-url.ts +0 -33
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { code, mdKeyValue, mdQuote, mdSection } from '../lib/format.js';
|
|
4
|
+
import { omitKeys, previewThenConfirm, validateArgs, type ToolContext } from '../lib/tool-runtime.js';
|
|
5
|
+
|
|
6
|
+
const LOCATION_ID_DESCRIPTION = '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.';
|
|
7
|
+
const LEGACY_LOCATION = {
|
|
8
|
+
workspace_id: z.string().optional(),
|
|
9
|
+
account_id: z.number().int().positive().optional(),
|
|
10
|
+
location_id: z.string().optional(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ManageReviewInput = {
|
|
14
|
+
action: 'suggest_reply' | 'reply' | 'delete_reply';
|
|
15
|
+
workspace_id?: string;
|
|
16
|
+
account_id?: number;
|
|
17
|
+
location_id?: string;
|
|
18
|
+
review_name?: string;
|
|
19
|
+
comment?: string;
|
|
20
|
+
review_text?: string;
|
|
21
|
+
star_rating?: number;
|
|
22
|
+
reviewer_name?: string;
|
|
23
|
+
business_name?: string;
|
|
24
|
+
tone?: 'professional' | 'friendly' | 'apologetic';
|
|
25
|
+
count?: number;
|
|
26
|
+
confirm?: boolean;
|
|
27
|
+
preview_id?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Everything the review endpoints accept, without the selector and confirm fields. */
|
|
31
|
+
function payloadOf(input: ManageReviewInput): Record<string, any> {
|
|
32
|
+
return omitKeys(input, ['action', 'confirm', 'preview_id']);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function locationRows(input: ManageReviewInput): Array<[string, string | undefined]> {
|
|
36
|
+
return [
|
|
37
|
+
['Account ID', input.account_id !== undefined ? code(input.account_id) : undefined],
|
|
38
|
+
['Location ID', input.location_id ? code(input.location_id) : undefined],
|
|
39
|
+
['Workspace', input.workspace_id ? code(input.workspace_id) : undefined],
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const manageGoogleBusinessReviewTool = {
|
|
44
|
+
name: 'manage_google_business_review',
|
|
45
|
+
description:
|
|
46
|
+
'Act on Google Business Profile reviews. action=suggest_reply (star_rating, optional review_text): short, brand-aware AI reply suggestions; uses the AI Caption Assist allowance and never posts. action=reply (review_name, comment): post or update the owner reply. action=delete_reply (review_name): delete the owner reply. reply and delete_reply use preview then confirm: call without confirm to get a preview and preview_id (it repeats the review name, location, and exact reply text; show the reviewer, rating, and review text from list_google_business_reviews alongside it), then call again with confirm=true and that preview_id.',
|
|
47
|
+
inputSchema: z.object({
|
|
48
|
+
action: z.enum(['suggest_reply', 'reply', 'delete_reply']).describe('suggest_reply, reply, or delete_reply.'),
|
|
49
|
+
workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
|
|
50
|
+
account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts (optional brand voice context for suggest_reply).'),
|
|
51
|
+
location_id: z.string().optional().describe(LOCATION_ID_DESCRIPTION),
|
|
52
|
+
review_name: z.string().min(1).optional().describe('reply and delete_reply: full Google review resource name.'),
|
|
53
|
+
comment: z.string().min(1).max(4096).optional().describe('reply: exact owner reply text to post.'),
|
|
54
|
+
review_text: z.string().optional().describe('suggest_reply: review text. Leave empty for rating-only reviews.'),
|
|
55
|
+
star_rating: z.number().min(1).max(5).optional().describe('suggest_reply: review rating from 1 to 5.'),
|
|
56
|
+
reviewer_name: z.string().optional(),
|
|
57
|
+
business_name: z.string().optional(),
|
|
58
|
+
tone: z.enum(['professional', 'friendly', 'apologetic']).optional(),
|
|
59
|
+
count: z.number().int().min(1).max(5).optional(),
|
|
60
|
+
confirm: z.boolean().optional().describe('reply and delete_reply: leave out to get a preview; true only with the preview_id after the user approved it.'),
|
|
61
|
+
preview_id: z.string().optional().describe('reply and delete_reply: the preview_id from the preview call. Required with confirm: true.'),
|
|
62
|
+
}),
|
|
63
|
+
/** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
|
|
64
|
+
legacySchemas: {
|
|
65
|
+
suggest_google_business_review_reply: z.object({
|
|
66
|
+
...LEGACY_LOCATION,
|
|
67
|
+
review_text: z.string().optional(),
|
|
68
|
+
star_rating: z.number().min(1).max(5),
|
|
69
|
+
reviewer_name: z.string().optional(),
|
|
70
|
+
business_name: z.string().optional(),
|
|
71
|
+
tone: z.enum(['professional', 'friendly', 'apologetic']).optional(),
|
|
72
|
+
count: z.number().int().min(1).max(5).optional(),
|
|
73
|
+
}),
|
|
74
|
+
reply_google_business_review: z.object({
|
|
75
|
+
...LEGACY_LOCATION,
|
|
76
|
+
review_name: z.string().min(1),
|
|
77
|
+
comment: z.string().min(1).max(4096),
|
|
78
|
+
confirm: z.literal(true),
|
|
79
|
+
}),
|
|
80
|
+
delete_google_business_review_reply: z.object({
|
|
81
|
+
...LEGACY_LOCATION,
|
|
82
|
+
review_name: z.string().min(1),
|
|
83
|
+
confirm: z.literal(true),
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async execute(client: PosterlyClient, input: ManageReviewInput, ctx: ToolContext) {
|
|
88
|
+
const { mode, args } = validateArgs(ctx, 'manage_google_business_review', input);
|
|
89
|
+
const call = args as ManageReviewInput;
|
|
90
|
+
if (mode === 'suggest_reply') return suggestReply(client, call as ManageReviewInput & { star_rating: number });
|
|
91
|
+
if (mode === 'delete_reply') {
|
|
92
|
+
return previewThenConfirm({
|
|
93
|
+
tool: 'manage_google_business_review',
|
|
94
|
+
action: 'delete_reply',
|
|
95
|
+
args,
|
|
96
|
+
nonce: false,
|
|
97
|
+
legacyRefusal: 'Refusing to delete a Google review reply without confirm=true after explicit user confirmation.',
|
|
98
|
+
ctx,
|
|
99
|
+
describe: async () => [
|
|
100
|
+
mdSection('Review', mdKeyValue([['Review', code(String(call.review_name))], ...locationRows(call)])),
|
|
101
|
+
'This deletes the owner reply from this review on Google. The review itself stays.',
|
|
102
|
+
].join('\n\n'),
|
|
103
|
+
run: async () => {
|
|
104
|
+
const result = await client.deleteGoogleBusinessReviewReply(payloadOf(call) as { review_name: string });
|
|
105
|
+
return result.message || 'Google Business review reply deleted successfully.';
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return previewThenConfirm({
|
|
110
|
+
tool: 'manage_google_business_review',
|
|
111
|
+
action: 'reply',
|
|
112
|
+
args,
|
|
113
|
+
nonce: true,
|
|
114
|
+
legacyRefusal: 'Refusing to post a Google review reply without confirm=true after explicit user confirmation.',
|
|
115
|
+
ctx,
|
|
116
|
+
describe: async () => [
|
|
117
|
+
mdSection('Review', mdKeyValue([['Review', code(String(call.review_name))], ...locationRows(call)])),
|
|
118
|
+
mdSection('Owner reply to post', mdQuote(String(call.comment ?? ''))),
|
|
119
|
+
'This posts publicly on Google as the business owner and replaces any existing owner reply on this review.',
|
|
120
|
+
].join('\n\n'),
|
|
121
|
+
run: async () => {
|
|
122
|
+
const result = await client.replyGoogleBusinessReview(payloadOf(call) as { review_name: string; comment: string });
|
|
123
|
+
return result.message || 'Google Business review reply posted successfully.';
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
async function suggestReply(client: PosterlyClient, input: ManageReviewInput & { star_rating: number }): Promise<string> {
|
|
130
|
+
const result = await client.suggestGoogleBusinessReviewReply(payloadOf(input) as { star_rating: number });
|
|
131
|
+
const suggestions = Array.isArray(result.suggestions) ? result.suggestions : [];
|
|
132
|
+
if (suggestions.length === 0) {
|
|
133
|
+
return 'No reply suggestions were generated.';
|
|
134
|
+
}
|
|
135
|
+
return [
|
|
136
|
+
`Suggested replies (${result.tone || 'professional'}, ${result.rating || input.star_rating} stars):`,
|
|
137
|
+
...suggestions.map((text: string, index: number) => `${index + 1}. ${text}`),
|
|
138
|
+
].join('\n');
|
|
139
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { code, mdKeyValue, mdSection } from '../lib/format.js';
|
|
4
|
+
import { omitKeys, previewThenConfirm, readFailedNote, readForPreview, validateArgs, type ToolContext } from '../lib/tool-runtime.js';
|
|
5
|
+
|
|
6
|
+
const scopeSchema = z.enum(['accounts:read', 'accounts:write', 'posts:read', 'posts:write', 'media:write', 'analytics:read']);
|
|
7
|
+
|
|
8
|
+
type ManageOAuthClientInput = {
|
|
9
|
+
action: 'create' | 'update' | 'delete';
|
|
10
|
+
client_id?: string;
|
|
11
|
+
client_name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
homepage?: string;
|
|
14
|
+
allowed_redirect_uris?: string[];
|
|
15
|
+
default_scopes?: string[];
|
|
16
|
+
is_active?: boolean;
|
|
17
|
+
confirm?: boolean;
|
|
18
|
+
preview_id?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const manageOAuthClientTool = {
|
|
22
|
+
name: 'manage_oauth_client',
|
|
23
|
+
description:
|
|
24
|
+
'Manage self-serve OAuth 2.1 + PKCE developer clients (public clients with no client secret; list them with list_oauth_clients). action=create: client_name, allowed_redirect_uris, and default_scopes; confirm=true only after the user confirms the name, redirect URIs, and scopes. action=update: client_id plus the changed fields; confirm=true after the user confirms the changes. action=delete: client_id; prevents new authorizations (existing access tokens stay revocable as API keys); preview then confirm: call without confirm to get a preview and preview_id, show it, then call again with confirm=true and that preview_id.',
|
|
25
|
+
inputSchema: z.object({
|
|
26
|
+
action: z.enum(['create', 'update', 'delete']).describe('create, update, or delete.'),
|
|
27
|
+
client_id: z.string().min(1).optional().describe('update and delete: the client_id from list_oauth_clients.'),
|
|
28
|
+
client_name: z.string().min(1).max(120).optional().describe('create (required) or update.'),
|
|
29
|
+
description: z.string().max(500).optional(),
|
|
30
|
+
homepage: z.string().url().optional(),
|
|
31
|
+
allowed_redirect_uris: z.array(z.string().url()).min(1).max(10).optional().describe('create (required) or update.'),
|
|
32
|
+
default_scopes: z.array(scopeSchema).min(1).optional().describe('create (required) or update.'),
|
|
33
|
+
is_active: z.boolean().optional(),
|
|
34
|
+
confirm: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('create and update: true after explicit user confirmation. delete: leave out to get a preview; true only with the preview_id after the user approved it.'),
|
|
38
|
+
preview_id: z.string().optional().describe('delete: the preview_id from the preview call. Required with confirm: true.'),
|
|
39
|
+
}),
|
|
40
|
+
/** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
|
|
41
|
+
legacySchemas: {
|
|
42
|
+
create_oauth_client: z.object({
|
|
43
|
+
client_name: z.string().min(1).max(120),
|
|
44
|
+
description: z.string().max(500).optional(),
|
|
45
|
+
homepage: z.string().url().optional(),
|
|
46
|
+
allowed_redirect_uris: z.array(z.string().url()).min(1).max(10),
|
|
47
|
+
default_scopes: z.array(scopeSchema).min(1),
|
|
48
|
+
is_active: z.boolean().optional(),
|
|
49
|
+
confirm: z.literal(true),
|
|
50
|
+
}),
|
|
51
|
+
update_oauth_client: z.object({
|
|
52
|
+
client_id: z.string().min(1),
|
|
53
|
+
client_name: z.string().min(1).max(120).optional(),
|
|
54
|
+
description: z.string().max(500).optional(),
|
|
55
|
+
homepage: z.string().url().optional(),
|
|
56
|
+
allowed_redirect_uris: z.array(z.string().url()).min(1).max(10).optional(),
|
|
57
|
+
default_scopes: z.array(scopeSchema).min(1).optional(),
|
|
58
|
+
is_active: z.boolean().optional(),
|
|
59
|
+
confirm: z.literal(true),
|
|
60
|
+
}),
|
|
61
|
+
delete_oauth_client: z.object({ client_id: z.string().min(1), confirm: z.literal(true) }),
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async execute(client: PosterlyClient, input: ManageOAuthClientInput, ctx: ToolContext) {
|
|
65
|
+
const { mode, args } = validateArgs(ctx, 'manage_oauth_client', input);
|
|
66
|
+
const call = args as ManageOAuthClientInput;
|
|
67
|
+
if (mode === 'create') {
|
|
68
|
+
if (call.confirm !== true) {
|
|
69
|
+
throw new Error('Refusing to create OAuth client without confirm=true after explicit user confirmation.');
|
|
70
|
+
}
|
|
71
|
+
const payload = omitKeys(call, ['action', 'confirm', 'preview_id']) as {
|
|
72
|
+
client_name: string;
|
|
73
|
+
allowed_redirect_uris: string[];
|
|
74
|
+
default_scopes: string[];
|
|
75
|
+
};
|
|
76
|
+
const result = await client.createOAuthClient(payload);
|
|
77
|
+
return `OAuth client created: ${result.client.client_name}\nclient_id: ${result.client.client_id}\nRedirect URIs: ${result.client.allowed_redirect_uris.join(', ')}`;
|
|
78
|
+
}
|
|
79
|
+
if (mode === 'update') {
|
|
80
|
+
if (call.confirm !== true) {
|
|
81
|
+
throw new Error('Refusing to update OAuth client without confirm=true after explicit user confirmation.');
|
|
82
|
+
}
|
|
83
|
+
const payload = omitKeys(call, ['action', 'confirm', 'preview_id', 'client_id']);
|
|
84
|
+
const result = await client.updateOAuthClient(String(call.client_id), payload);
|
|
85
|
+
return `OAuth client updated: ${result.client.client_name} (${result.client.client_id}) active=${result.client.is_active}`;
|
|
86
|
+
}
|
|
87
|
+
return previewThenConfirm({
|
|
88
|
+
tool: 'manage_oauth_client',
|
|
89
|
+
action: 'delete',
|
|
90
|
+
args,
|
|
91
|
+
nonce: false,
|
|
92
|
+
legacyRefusal: 'Refusing to delete OAuth client without confirm=true after explicit user confirmation.',
|
|
93
|
+
ctx,
|
|
94
|
+
describe: async () => {
|
|
95
|
+
const read = await readForPreview(() => client.listOAuthClients());
|
|
96
|
+
const found = read.ok ? read.data.clients.find((item) => String(item.client_id) === String(call.client_id)) : null;
|
|
97
|
+
const details = found
|
|
98
|
+
? mdSection('OAuth client', mdKeyValue([
|
|
99
|
+
['Name', found.client_name],
|
|
100
|
+
['Client ID', code(found.client_id)],
|
|
101
|
+
['Redirect URIs', found.allowed_redirect_uris.join(', ')],
|
|
102
|
+
['Active', found.is_active],
|
|
103
|
+
]))
|
|
104
|
+
: [
|
|
105
|
+
read.ok ? '_This client was not found in your OAuth client list, so the preview shows the request as given._' : readFailedNote(read.error),
|
|
106
|
+
mdKeyValue([['Client ID', code(String(call.client_id))]]),
|
|
107
|
+
].join('\n\n');
|
|
108
|
+
return [details, 'This deletes the OAuth client: nobody can authorize it again. Existing access tokens stay revocable as API keys.'].join('\n\n');
|
|
109
|
+
},
|
|
110
|
+
run: async () => {
|
|
111
|
+
const result = await client.deleteOAuthClient(String(call.client_id));
|
|
112
|
+
return `OAuth client deleted: ${result.client_id}`;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
import { code, mdKeyValue, mdSection } from '../lib/format.js';
|
|
4
|
+
import { omitKeys, previewThenConfirm, readFailedNote, readForPreview, validateArgs, type ToolContext } from '../lib/tool-runtime.js';
|
|
5
|
+
import { webhookEventSchema } from './webhook-events.js';
|
|
6
|
+
|
|
7
|
+
type ManageWebhookInput = {
|
|
8
|
+
action: 'update' | 'delete' | 'test';
|
|
9
|
+
webhook_id: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
events?: string[];
|
|
12
|
+
workspace_id?: string | null;
|
|
13
|
+
description?: string | null;
|
|
14
|
+
is_active?: boolean;
|
|
15
|
+
confirm?: boolean;
|
|
16
|
+
preview_id?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const manageWebhookTool = {
|
|
20
|
+
name: 'manage_webhook',
|
|
21
|
+
description:
|
|
22
|
+
'Change a webhook subscription (list them first with list_webhooks). action=update (webhook_id): change URL, events, workspace, description, or active state; outbound side effects, so pass confirm=true only after explicit confirmation. action=delete (webhook_id): remove the subscription; preview then confirm: call without confirm to get a preview and preview_id, show the URL, events, and workspace, then call again with confirm=true and that preview_id. action=test (webhook_id): send a signed webhook.test delivery; confirm=true after explicit confirmation.',
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
action: z.enum(['update', 'delete', 'test']).describe('update, delete, or test.'),
|
|
25
|
+
webhook_id: z.string().describe('Webhook ID from list_webhooks.'),
|
|
26
|
+
url: z.string().url().optional().describe('update only.'),
|
|
27
|
+
events: z.array(webhookEventSchema).optional().describe('update only.'),
|
|
28
|
+
workspace_id: z.union([z.string(), z.null()]).optional().describe('update only. null clears the workspace.'),
|
|
29
|
+
description: z.union([z.string().max(240), z.null()]).optional().describe('update only.'),
|
|
30
|
+
is_active: z.boolean().optional().describe('update only.'),
|
|
31
|
+
confirm: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('update and test: true after the user explicitly confirms. delete: leave out to get a preview; true only with the preview_id after the user approved it.'),
|
|
35
|
+
preview_id: z.string().optional().describe('delete: the preview_id from the preview call. Required with confirm: true.'),
|
|
36
|
+
}),
|
|
37
|
+
/** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
|
|
38
|
+
legacySchemas: {
|
|
39
|
+
update_webhook: z.object({
|
|
40
|
+
webhook_id: z.string(),
|
|
41
|
+
url: z.string().url().optional(),
|
|
42
|
+
events: z.array(webhookEventSchema).optional(),
|
|
43
|
+
workspace_id: z.union([z.string(), z.null()]).optional(),
|
|
44
|
+
description: z.union([z.string().max(240), z.null()]).optional(),
|
|
45
|
+
is_active: z.boolean().optional(),
|
|
46
|
+
confirm: z.literal(true),
|
|
47
|
+
}),
|
|
48
|
+
delete_webhook: z.object({ webhook_id: z.string(), confirm: z.literal(true) }),
|
|
49
|
+
test_webhook: z.object({ webhook_id: z.string(), confirm: z.literal(true) }),
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async execute(client: PosterlyClient, input: ManageWebhookInput, ctx: ToolContext) {
|
|
53
|
+
const { mode, args } = validateArgs(ctx, 'manage_webhook', input);
|
|
54
|
+
const call = args as ManageWebhookInput;
|
|
55
|
+
if (mode === 'update') {
|
|
56
|
+
if (call.confirm !== true) {
|
|
57
|
+
throw new Error('Refusing to update webhook without confirm=true after explicit user confirmation.');
|
|
58
|
+
}
|
|
59
|
+
const payload = omitKeys(call, ['action', 'confirm', 'preview_id', 'webhook_id']);
|
|
60
|
+
const result = await client.updateWebhook(call.webhook_id, payload);
|
|
61
|
+
const active = result.webhook.is_active ? 'active' : 'inactive';
|
|
62
|
+
return `Webhook updated: ${result.webhook.id}\nURL: ${result.webhook.url}\nState: ${active}\nEvents: ${result.webhook.events.join(', ')}`;
|
|
63
|
+
}
|
|
64
|
+
if (mode === 'test') {
|
|
65
|
+
if (call.confirm !== true) {
|
|
66
|
+
throw new Error('Refusing to test webhook without confirm=true after explicit user confirmation.');
|
|
67
|
+
}
|
|
68
|
+
const result = await client.testWebhook(call.webhook_id);
|
|
69
|
+
const delivery = result.delivery || {};
|
|
70
|
+
return `Webhook test sent.\nDelivery: ${JSON.stringify(delivery, null, 2)}`;
|
|
71
|
+
}
|
|
72
|
+
return previewThenConfirm({
|
|
73
|
+
tool: 'manage_webhook',
|
|
74
|
+
action: 'delete',
|
|
75
|
+
args,
|
|
76
|
+
nonce: false,
|
|
77
|
+
legacyRefusal: 'Refusing to delete webhook without confirm=true after explicit user confirmation.',
|
|
78
|
+
ctx,
|
|
79
|
+
describe: async () => {
|
|
80
|
+
// There is no GET /webhooks/:id, so the preview finds it in the list.
|
|
81
|
+
const read = await readForPreview(() => client.listWebhooks({}));
|
|
82
|
+
const webhook = read.ok ? read.data.webhooks.find((item) => String(item.id) === String(call.webhook_id)) : null;
|
|
83
|
+
const details = webhook
|
|
84
|
+
? mdSection('Webhook', mdKeyValue([
|
|
85
|
+
['Webhook ID', code(webhook.id)],
|
|
86
|
+
['URL', webhook.url],
|
|
87
|
+
['Events', webhook.events?.length ? webhook.events.join(', ') : 'default events'],
|
|
88
|
+
['State', webhook.is_active ? 'active' : 'inactive'],
|
|
89
|
+
['Workspace', webhook.workspace_id ? code(webhook.workspace_id) : 'all workspaces'],
|
|
90
|
+
]))
|
|
91
|
+
: [
|
|
92
|
+
read.ok ? '_This webhook was not found in your webhook list, so the preview shows the request as given._' : readFailedNote(read.error),
|
|
93
|
+
mdKeyValue([['Webhook ID', code(call.webhook_id)]]),
|
|
94
|
+
].join('\n\n');
|
|
95
|
+
return [details, 'This deletes the webhook subscription. posterly stops sending events to its URL.'].join('\n\n');
|
|
96
|
+
},
|
|
97
|
+
run: async () => {
|
|
98
|
+
const result = await client.deleteWebhook(call.webhook_id);
|
|
99
|
+
return `Webhook deleted: ${result.id}`;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
package/src/tools/update-post.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
3
|
import { code, formatDateTime, mdKeyValue, mdQuote, mdSection, mdTitle, statusLabel } from '../lib/format.js';
|
|
4
|
+
import { validateArgs, type ToolContext } from '../lib/tool-runtime.js';
|
|
4
5
|
|
|
5
6
|
const mediaAccessibilitySchema = z.array(z.object({
|
|
6
7
|
index: z.number().int().nonnegative(),
|
|
@@ -110,11 +111,29 @@ const platformSettingsSchema = z.object({
|
|
|
110
111
|
ai_generated: z.boolean().optional(),
|
|
111
112
|
}).passthrough();
|
|
112
113
|
|
|
114
|
+
type EditInput = {
|
|
115
|
+
post_id: number;
|
|
116
|
+
caption?: string;
|
|
117
|
+
scheduled_at?: string;
|
|
118
|
+
media_url?: string;
|
|
119
|
+
media_urls?: string[];
|
|
120
|
+
post_type?: string;
|
|
121
|
+
instagram_settings?: z.infer<typeof instagramSettingsSchema>;
|
|
122
|
+
platform_settings?: z.infer<typeof platformSettingsSchema>;
|
|
123
|
+
confirm: true;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type UpdatePostInput = EditInput & {
|
|
127
|
+
status?: 'scheduled' | 'paused' | 'draft';
|
|
128
|
+
release_id?: string;
|
|
129
|
+
group_id?: string;
|
|
130
|
+
};
|
|
131
|
+
|
|
113
132
|
export const updatePostTool = {
|
|
114
133
|
name: 'update_post',
|
|
115
134
|
description:
|
|
116
|
-
'
|
|
117
|
-
'REQUIRED BEFORE CALLING: Show the user a side-by-side of the CURRENT post (caption, scheduled time, media) and the PROPOSED
|
|
135
|
+
'Change a scheduled or draft post. DESTRUCTIVE WRITE. Edit (default): caption, media, schedule, post_type, or settings; overwrites the existing post; cannot edit published or currently publishing posts; if a client has already approved the post, a caption edit reopens caption approval, a media or thumbnail edit reopens asset approval, and a reschedule, account change or settings change reopens both. status (scheduled, paused, or draft, with scheduled_at when scheduling a draft): pause, resume, or move back to draft; send it on its own. release_id (with optional group_id): set or repair external release/group IDs; send it on its own.\n\n' +
|
|
136
|
+
'REQUIRED BEFORE CALLING: Show the user a side-by-side of the CURRENT post (caption, scheduled time, media, status) and the PROPOSED change, and get explicit confirmation ("yes update it", "apply those changes") before calling. Do not auto-edit posts based on a general instruction - each change needs its own confirmation.',
|
|
118
137
|
inputSchema: z.object({
|
|
119
138
|
post_id: z.number().describe('The post ID to update'),
|
|
120
139
|
caption: z.string().optional().describe('New caption/text content'),
|
|
@@ -136,48 +155,96 @@ export const updatePostTool = {
|
|
|
136
155
|
reel_thumb_offset: z.number().nonnegative().optional().describe('Frame offset in milliseconds to use as the reel cover.'),
|
|
137
156
|
instagram_settings: instagramSettingsSchema
|
|
138
157
|
.optional()
|
|
139
|
-
.describe('Instagram-specific updates. Distinguish caption @mentions, native media user_tags, and collaborators. Native media tags work with eligible direct or Meta-linked accounts; collaborators require an eligible Meta-linked account. Call
|
|
158
|
+
.describe('Instagram-specific updates. Distinguish caption @mentions, native media user_tags, and collaborators. Native media tags work with eligible direct or Meta-linked accounts; collaborators require an eligible Meta-linked account. Call list_platforms with view=schema and account_id first.'),
|
|
140
159
|
platform_settings: platformSettingsSchema
|
|
141
160
|
.optional()
|
|
142
161
|
.describe('Platform-specific settings to merge into the post. Prefer this over raw metadata.'),
|
|
143
|
-
|
|
162
|
+
status: z
|
|
163
|
+
.enum(['scheduled', 'paused', 'draft'])
|
|
164
|
+
.optional()
|
|
165
|
+
.describe('Change the post status instead of editing it. Send with post_id (and scheduled_at when scheduling a draft) only.'),
|
|
166
|
+
release_id: z
|
|
167
|
+
.string()
|
|
168
|
+
.min(1)
|
|
169
|
+
.max(160)
|
|
170
|
+
.optional()
|
|
171
|
+
.describe('Set an external release ID instead of editing the post. Send with post_id and optional group_id only.'),
|
|
172
|
+
group_id: z.string().min(1).max(160).optional().describe('With release_id only: an external group/campaign ID.'),
|
|
173
|
+
confirm: z.literal(true).describe('Must be true after explicit user confirmation of the proposed change.'),
|
|
144
174
|
}),
|
|
145
175
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
post_id: number
|
|
150
|
-
|
|
151
|
-
scheduled_at
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new Error('Refusing to update post without confirm=true after explicit user confirmation.');
|
|
162
|
-
}
|
|
163
|
-
const { post_id, confirm: _confirm, instagram_settings, platform_settings, ...updates } = input;
|
|
164
|
-
const payload = {
|
|
165
|
-
...updates,
|
|
166
|
-
...(platform_settings || instagram_settings
|
|
167
|
-
? { settings: { ...(instagram_settings ? { __type: 'instagram', ...instagram_settings } : {}), ...(platform_settings || {}) } }
|
|
168
|
-
: {}),
|
|
169
|
-
};
|
|
170
|
-
const result = await client.updatePost(post_id, payload);
|
|
171
|
-
const p = result.post as Record<string, any>;
|
|
176
|
+
/** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
|
|
177
|
+
legacySchemas: {
|
|
178
|
+
update_post_status: z.object({
|
|
179
|
+
post_id: z.number(),
|
|
180
|
+
status: z.enum(['scheduled', 'paused', 'draft']),
|
|
181
|
+
scheduled_at: z.string().optional(),
|
|
182
|
+
confirm: z.boolean(),
|
|
183
|
+
}),
|
|
184
|
+
update_post_release_id: z.object({
|
|
185
|
+
post_id: z.number().int().positive(),
|
|
186
|
+
release_id: z.string().min(1).max(160),
|
|
187
|
+
group_id: z.string().min(1).max(160).optional(),
|
|
188
|
+
confirm: z.literal(true),
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
172
191
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
['Type', p.post_type || 'unknown'],
|
|
179
|
-
]),
|
|
180
|
-
mdSection('Caption', p.content ? mdQuote(p.content) : '_(empty)_'),
|
|
181
|
-
].join('\n\n');
|
|
192
|
+
async execute(client: PosterlyClient, input: UpdatePostInput, ctx: ToolContext) {
|
|
193
|
+
const { mode, args } = validateArgs(ctx, 'update_post', input);
|
|
194
|
+
if (mode === 'status') return updateStatus(client, args as { post_id: number; status: 'scheduled' | 'paused' | 'draft'; scheduled_at?: string; confirm: boolean });
|
|
195
|
+
if (mode === 'release_id') return updateReleaseId(client, args as { post_id: number; release_id: string; group_id?: string; confirm: true });
|
|
196
|
+
return editPost(client, args as EditInput);
|
|
182
197
|
},
|
|
183
198
|
};
|
|
199
|
+
|
|
200
|
+
async function editPost(client: PosterlyClient, input: EditInput): Promise<string> {
|
|
201
|
+
if (input.confirm !== true) {
|
|
202
|
+
throw new Error('Refusing to update post without confirm=true after explicit user confirmation.');
|
|
203
|
+
}
|
|
204
|
+
const { post_id, confirm: _confirm, preview_id: _previewId, instagram_settings, platform_settings, ...updates } = input as EditInput & { preview_id?: string };
|
|
205
|
+
const payload = {
|
|
206
|
+
...updates,
|
|
207
|
+
...(platform_settings || instagram_settings
|
|
208
|
+
? { settings: { ...(instagram_settings ? { __type: 'instagram', ...instagram_settings } : {}), ...(platform_settings || {}) } }
|
|
209
|
+
: {}),
|
|
210
|
+
};
|
|
211
|
+
const result = await client.updatePost(post_id, payload);
|
|
212
|
+
const p = result.post as Record<string, any>;
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
mdTitle(`✅ Post ${code(p.id)} updated`),
|
|
216
|
+
mdKeyValue([
|
|
217
|
+
['Status', statusLabel(p.status)],
|
|
218
|
+
['Scheduled', formatDateTime(p.scheduled_at)],
|
|
219
|
+
['Type', p.post_type || 'unknown'],
|
|
220
|
+
]),
|
|
221
|
+
mdSection('Caption', p.content ? mdQuote(p.content) : '_(empty)_'),
|
|
222
|
+
].join('\n\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function updateStatus(
|
|
226
|
+
client: PosterlyClient,
|
|
227
|
+
input: { post_id: number; status: 'scheduled' | 'paused' | 'draft'; scheduled_at?: string; confirm: boolean },
|
|
228
|
+
): Promise<string> {
|
|
229
|
+
if (input.confirm !== true) {
|
|
230
|
+
throw new Error('Refusing to update post status without confirm=true after explicit user confirmation.');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = await client.updatePostStatus(input.post_id, {
|
|
234
|
+
status: input.status,
|
|
235
|
+
scheduled_at: input.scheduled_at,
|
|
236
|
+
});
|
|
237
|
+
const transition = result.transition || {};
|
|
238
|
+
return `Post #${input.post_id} status changed from ${transition.old_status || 'unknown'} to ${transition.new_status || input.status}.`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function updateReleaseId(
|
|
242
|
+
client: PosterlyClient,
|
|
243
|
+
input: { post_id: number; release_id: string; group_id?: string; confirm: true },
|
|
244
|
+
): Promise<string> {
|
|
245
|
+
const result = await client.updatePostReleaseId(input.post_id, {
|
|
246
|
+
release_id: input.release_id,
|
|
247
|
+
group_id: input.group_id,
|
|
248
|
+
});
|
|
249
|
+
return JSON.stringify(result, null, 2);
|
|
250
|
+
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
3
|
import { mdError, mdSuccess } from '../lib/format.js';
|
|
4
|
+
import { validateArgs, type ToolContext } from '../lib/tool-runtime.js';
|
|
5
|
+
|
|
6
|
+
type UploadMediaInput = {
|
|
7
|
+
file_path?: string;
|
|
8
|
+
base64_data?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
filename?: string;
|
|
11
|
+
content_type?: string;
|
|
12
|
+
};
|
|
4
13
|
|
|
5
14
|
export const uploadMediaTool = {
|
|
6
15
|
name: 'upload_media',
|
|
7
16
|
description:
|
|
8
|
-
'Upload an image or video
|
|
9
|
-
'This writes to the user\'s media storage (counts against quota) but does NOT publish the file anywhere. Uploading is safe - it\'s the subsequent `create_post` call that publishes content, and that tool has its own confirmation requirements
|
|
10
|
-
'IMPORTANT: If the user shares an image in the chat, use base64_data (not file_path) since chat-uploaded images are not on the local filesystem. Only use file_path when the user provides an actual local filesystem path - never guess paths.',
|
|
17
|
+
'Upload an image or video to posterly storage and get a URL for create_post. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM. Images up to 10MB, videos up to 50MB. Three ways in: file_path (an explicit local path the user gave you), base64_data with filename (PREFERRED for images shared in chat, which are not on the local filesystem), or url (a public HTTPS image or video; local/private network URLs are blocked).\n\n' +
|
|
18
|
+
'This writes to the user\'s media storage (counts against quota) but does NOT publish the file anywhere. Uploading is safe - it\'s the subsequent `create_post` call that publishes content, and that tool has its own confirmation requirements. Never guess file paths. You can also pass public HTTP(S) media URLs directly to create_post/update_post; use url here when you want to upload once and reuse the returned posterly URL.',
|
|
11
19
|
inputSchema: z.object({
|
|
12
20
|
file_path: z
|
|
13
21
|
.string()
|
|
@@ -17,38 +25,78 @@ export const uploadMediaTool = {
|
|
|
17
25
|
.string()
|
|
18
26
|
.optional()
|
|
19
27
|
.describe('Base64-encoded file data. PREFERRED for images shared directly in chat - extract the image data as base64 and pass it here.'),
|
|
28
|
+
url: z
|
|
29
|
+
.string()
|
|
30
|
+
.url()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Public HTTPS URL for the image or video to copy into posterly storage. Local/private network URLs are blocked.'),
|
|
20
33
|
filename: z
|
|
21
34
|
.string()
|
|
22
|
-
.
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Filename with extension (e.g. photo.jpg, video.mp4). Required with file_path or base64_data; optional override with url.'),
|
|
23
37
|
content_type: z
|
|
24
38
|
.string()
|
|
25
39
|
.optional()
|
|
26
40
|
.describe('MIME type (auto-detected from filename if omitted)'),
|
|
27
41
|
}),
|
|
42
|
+
/** The old tools' exact input schemas, for alias calls (see alias-transport.ts). */
|
|
43
|
+
legacySchemas: {
|
|
44
|
+
upload_media_from_url: z.object({
|
|
45
|
+
url: z.string().url(),
|
|
46
|
+
filename: z.string().optional(),
|
|
47
|
+
content_type: z.string().optional(),
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
28
50
|
|
|
29
|
-
async execute(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
base64_data?: string;
|
|
34
|
-
filename: string;
|
|
35
|
-
content_type?: string;
|
|
36
|
-
}
|
|
37
|
-
) {
|
|
38
|
-
if (!input.file_path && !input.base64_data) {
|
|
39
|
-
return mdError('Provide either `file_path` or `base64_data`.');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const result = await client.uploadMedia({
|
|
43
|
-
filePath: input.file_path,
|
|
44
|
-
base64Data: input.base64_data,
|
|
45
|
-
filename: input.filename,
|
|
46
|
-
contentType: input.content_type,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
return mdSuccess('Media uploaded', [
|
|
50
|
-
['URL', result.url],
|
|
51
|
-
['Storage path', result.path],
|
|
52
|
-
], 'Use this URL as `media_url` when creating a post.');
|
|
51
|
+
async execute(client: PosterlyClient, input: UploadMediaInput, ctx: ToolContext) {
|
|
52
|
+
const { mode, args } = validateArgs(ctx, 'upload_media', input);
|
|
53
|
+
if (mode === 'url') return uploadFromUrl(client, args as { url: string; filename?: string; content_type?: string });
|
|
54
|
+
return uploadFile(client, args as UploadMediaInput & { filename: string });
|
|
53
55
|
},
|
|
54
56
|
};
|
|
57
|
+
|
|
58
|
+
async function uploadFile(
|
|
59
|
+
client: PosterlyClient,
|
|
60
|
+
input: {
|
|
61
|
+
file_path?: string;
|
|
62
|
+
base64_data?: string;
|
|
63
|
+
filename: string;
|
|
64
|
+
content_type?: string;
|
|
65
|
+
},
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
if (!input.file_path && !input.base64_data) {
|
|
68
|
+
return mdError('Provide either `file_path` or `base64_data`.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await client.uploadMedia({
|
|
72
|
+
filePath: input.file_path,
|
|
73
|
+
base64Data: input.base64_data,
|
|
74
|
+
filename: input.filename,
|
|
75
|
+
contentType: input.content_type,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return mdSuccess('Media uploaded', [
|
|
79
|
+
['URL', result.url],
|
|
80
|
+
['Storage path', result.path],
|
|
81
|
+
], 'Use this URL as `media_url` when creating a post.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function uploadFromUrl(
|
|
85
|
+
client: PosterlyClient,
|
|
86
|
+
input: { url: string; filename?: string; content_type?: string },
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const result = await client.uploadMediaFromUrl({
|
|
89
|
+
url: input.url,
|
|
90
|
+
filename: input.filename,
|
|
91
|
+
contentType: input.content_type,
|
|
92
|
+
});
|
|
93
|
+
return [
|
|
94
|
+
'Remote media uploaded successfully.',
|
|
95
|
+
`• URL: ${result.url}`,
|
|
96
|
+
`• Source URL: ${result.source_url}`,
|
|
97
|
+
`• Content type: ${result.content_type}`,
|
|
98
|
+
`• Size: ${result.size} bytes`,
|
|
99
|
+
'',
|
|
100
|
+
'Use this URL as media_url when creating a post.',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|