@agentforge-io/connectors-meta 0.1.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.
@@ -0,0 +1,362 @@
1
+ "use strict";
2
+ /**
3
+ * Facebook Pages tools.
4
+ *
5
+ * Five tools for managing a Page's published content + engagement:
6
+ *
7
+ * 1. fb_list_posts — Page's own posts, newest first
8
+ * 2. fb_get_post — single post with engagement breakdown
9
+ * 3. fb_create_post — publish or schedule a text/link post
10
+ * 4. fb_list_comments — comments on a post
11
+ * 5. fb_reply_comment — reply nested under a comment
12
+ *
13
+ * Token model:
14
+ * - `ctx.getAccessToken()` returns the PAGE access token.
15
+ * - `ctx.metadata.pageId` is the Page id — every endpoint here
16
+ * routes through it.
17
+ *
18
+ * Why `/posts` (Page-authored) and not `/feed` (everything):
19
+ * - `/{page-id}/feed` returns posts the Page made PLUS posts by
20
+ * other people on the Page wall. Agents usually want "what did
21
+ * WE publish?" so `/posts` is the right default; if we ever need
22
+ * the wall, a future `fb_list_feed` can take the same shape.
23
+ *
24
+ * Scheduled posts:
25
+ * - Setting `published: false` + `scheduled_publish_time` (unix
26
+ * seconds) defers publication. Window: 10 minutes to 6 months
27
+ * from now (Meta hard limit; sending outside returns code 100).
28
+ *
29
+ * Media:
30
+ * - This step covers text + link posts only. Photo/video uploads
31
+ * require a separate /photos or /videos endpoint with multipart
32
+ * bodies, which deserves its own tool (`fb_create_photo_post`)
33
+ * and isn't part of the MVP. Posting an external image via the
34
+ * `link` field is supported — Meta scrapes the URL for OG tags
35
+ * and renders the link preview.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.fbReplyCommentTool = exports.fbListCommentsTool = exports.fbCreatePostTool = exports.fbGetPostTool = exports.fbListPostsTool = void 0;
39
+ const http_1 = require("../http");
40
+ const _shared_1 = require("./_shared");
41
+ exports.fbListPostsTool = {
42
+ definition: {
43
+ name: 'fb_list_posts',
44
+ description: "List the connected Facebook Page's own published posts, newest " +
45
+ 'first. Returns post id, message, link, created time, permalink, ' +
46
+ 'is-published flag, scheduled time (when applicable), and ' +
47
+ 'engagement totals (likes, comments, shares).',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ limit: {
52
+ type: 'number',
53
+ description: 'Max posts (1-50, default 20).',
54
+ minimum: 1,
55
+ maximum: 50,
56
+ },
57
+ cursor: {
58
+ type: 'string',
59
+ description: 'Pagination cursor from a previous call.',
60
+ },
61
+ includeUnpublished: {
62
+ type: 'boolean',
63
+ description: 'When true, includes scheduled + draft posts that are not ' +
64
+ 'yet visible. Default false (published only).',
65
+ },
66
+ },
67
+ },
68
+ },
69
+ build: (ctx) => async (input) => {
70
+ const pageId = (0, _shared_1.resolvePageId)(ctx);
71
+ const accessToken = await ctx.getAccessToken();
72
+ const limit = Math.min(Math.max(Number(input.limit ?? 20), 1), 50);
73
+ const result = await (0, http_1.metaGet)({
74
+ accessToken,
75
+ // Meta exposes scheduled/draft posts only via the alternate
76
+ // `promotable_posts` edge when `is_published=false` is requested.
77
+ // For the published-only default we use the standard `/posts`.
78
+ path: input.includeUnpublished
79
+ ? `/${encodeURIComponent(pageId)}/promotable_posts`
80
+ : `/${encodeURIComponent(pageId)}/posts`,
81
+ query: {
82
+ fields: 'id,message,story,created_time,permalink_url,link,scheduled_publish_time,is_published,likes.summary(true).limit(0),comments.summary(true).limit(0),shares',
83
+ limit,
84
+ after: input.cursor,
85
+ },
86
+ });
87
+ return JSON.stringify({
88
+ posts: result.data.map((p) => ({
89
+ id: p.id,
90
+ message: p.message,
91
+ story: p.story,
92
+ link: p.link,
93
+ permalink: p.permalink_url,
94
+ createdAt: p.created_time,
95
+ scheduledPublishTime: p.scheduled_publish_time,
96
+ isPublished: p.is_published ?? true,
97
+ engagement: {
98
+ likes: p.likes?.summary?.total_count ?? 0,
99
+ comments: p.comments?.summary?.total_count ?? 0,
100
+ shares: p.shares?.count ?? 0,
101
+ },
102
+ })),
103
+ pageInfo: {
104
+ nextCursor: result.paging?.cursors?.after,
105
+ hasNextPage: Boolean(result.paging?.next),
106
+ },
107
+ });
108
+ },
109
+ };
110
+ exports.fbGetPostTool = {
111
+ definition: {
112
+ name: 'fb_get_post',
113
+ description: 'Fetch a single Facebook Page post with full engagement + ' +
114
+ 'attachments. Returns message, link, permalink, status type, ' +
115
+ 'attachment metadata (type, URL, title, media URL), and ' +
116
+ 'engagement totals.',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {
120
+ postId: {
121
+ type: 'string',
122
+ description: 'Post id from `fb_list_posts`.',
123
+ },
124
+ },
125
+ required: ['postId'],
126
+ },
127
+ },
128
+ build: (ctx) => async (input) => {
129
+ const postId = String(input.postId ?? '').trim();
130
+ if (!postId)
131
+ throw new Error('postId is required.');
132
+ const accessToken = await ctx.getAccessToken();
133
+ const post = await (0, http_1.metaGet)({
134
+ accessToken,
135
+ path: `/${encodeURIComponent(postId)}`,
136
+ query: {
137
+ fields: 'id,message,story,created_time,updated_time,permalink_url,link,scheduled_publish_time,is_published,status_type,attachments{type,media_type,url,title,description,media},likes.summary(true).limit(0),comments.summary(true).limit(0),shares,reactions.summary(true).limit(0)',
138
+ },
139
+ });
140
+ return JSON.stringify({
141
+ id: post.id,
142
+ message: post.message,
143
+ story: post.story,
144
+ link: post.link,
145
+ permalink: post.permalink_url,
146
+ createdAt: post.created_time,
147
+ updatedAt: post.updated_time,
148
+ statusType: post.status_type,
149
+ scheduledPublishTime: post.scheduled_publish_time,
150
+ isPublished: post.is_published ?? true,
151
+ attachments: post.attachments?.data?.map((a) => ({
152
+ type: a.type,
153
+ mediaType: a.media_type,
154
+ url: a.url,
155
+ title: a.title,
156
+ description: a.description,
157
+ mediaUrl: a.media?.image?.src ?? a.media?.source,
158
+ })) ?? [],
159
+ engagement: {
160
+ likes: post.likes?.summary?.total_count ?? 0,
161
+ comments: post.comments?.summary?.total_count ?? 0,
162
+ shares: post.shares?.count ?? 0,
163
+ reactions: post.reactions?.summary?.total_count ?? 0,
164
+ },
165
+ });
166
+ },
167
+ };
168
+ /** Hard window Meta enforces on `scheduled_publish_time`:
169
+ * - earliest: 10 minutes from now
170
+ * - latest: 6 months (≈ 180 days) from now
171
+ * Outside the window returns code 100 with a useless message; we
172
+ * pre-check to give the agent a clearer error. */
173
+ const MIN_SCHEDULE_SECONDS = 10 * 60;
174
+ const MAX_SCHEDULE_SECONDS = 180 * 24 * 60 * 60;
175
+ exports.fbCreatePostTool = {
176
+ definition: {
177
+ name: 'fb_create_post',
178
+ description: 'Publish (or schedule) a text/link post on the connected ' +
179
+ 'Facebook Page. For a link post, set `link` — Meta scrapes the ' +
180
+ "URL's OG tags to render the preview. For a scheduled post, set " +
181
+ '`scheduledPublishTime` (unix seconds, 10min–6mo from now). ' +
182
+ 'Returns the new post id.',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: {
186
+ message: {
187
+ type: 'string',
188
+ description: 'Post body. At least one of `message` or `link` is required.',
189
+ },
190
+ link: {
191
+ type: 'string',
192
+ description: 'URL to share. Meta scrapes OG tags and renders a link card. ' +
193
+ "At least one of `message` or `link` is required.",
194
+ },
195
+ scheduledPublishTime: {
196
+ type: 'number',
197
+ description: 'Unix seconds at which to publish. Must be between 10 ' +
198
+ 'minutes and 6 months from now. When omitted, the post ' +
199
+ 'publishes immediately.',
200
+ },
201
+ },
202
+ },
203
+ },
204
+ build: (ctx) => async (input) => {
205
+ const pageId = (0, _shared_1.resolvePageId)(ctx);
206
+ const accessToken = await ctx.getAccessToken();
207
+ const message = input.message
208
+ ? String(input.message).trim()
209
+ : undefined;
210
+ const link = input.link ? String(input.link).trim() : undefined;
211
+ if (!message && !link) {
212
+ throw new Error('At least one of `message` or `link` must be provided.');
213
+ }
214
+ let scheduledPublishTime;
215
+ let published = true;
216
+ if (input.scheduledPublishTime !== undefined) {
217
+ scheduledPublishTime = Number(input.scheduledPublishTime);
218
+ const now = Math.floor(Date.now() / 1000);
219
+ const delta = scheduledPublishTime - now;
220
+ if (!Number.isFinite(scheduledPublishTime) || delta < MIN_SCHEDULE_SECONDS) {
221
+ throw new Error('scheduledPublishTime must be at least 10 minutes in the future ' +
222
+ `(unix seconds; received ${input.scheduledPublishTime}, now=${now}).`);
223
+ }
224
+ if (delta > MAX_SCHEDULE_SECONDS) {
225
+ throw new Error('scheduledPublishTime must be within 6 months of now.');
226
+ }
227
+ published = false;
228
+ }
229
+ const result = await (0, http_1.metaPost)({
230
+ accessToken,
231
+ path: `/${encodeURIComponent(pageId)}/feed`,
232
+ body: {
233
+ ...(message ? { message } : {}),
234
+ ...(link ? { link } : {}),
235
+ published,
236
+ ...(scheduledPublishTime
237
+ ? { scheduled_publish_time: scheduledPublishTime }
238
+ : {}),
239
+ },
240
+ });
241
+ return JSON.stringify({
242
+ postId: result.id,
243
+ published,
244
+ scheduledPublishTime,
245
+ });
246
+ },
247
+ };
248
+ exports.fbListCommentsTool = {
249
+ definition: {
250
+ name: 'fb_list_comments',
251
+ description: 'List comments on a Facebook Page post (or replies under a ' +
252
+ 'specific parent comment). Returns id, message, author, like ' +
253
+ 'count, reply count, hidden flag, parent id, and created time.',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: {
257
+ postId: {
258
+ type: 'string',
259
+ description: 'Post id from `fb_list_posts`. Required when ' +
260
+ '`parentCommentId` is not set.',
261
+ },
262
+ parentCommentId: {
263
+ type: 'string',
264
+ description: 'Comment id to read replies under. When set, `postId` is ' +
265
+ 'ignored.',
266
+ },
267
+ order: {
268
+ type: 'string',
269
+ enum: ['chronological', 'reverse_chronological'],
270
+ description: 'Sort order. Default `reverse_chronological` (newest first).',
271
+ },
272
+ limit: {
273
+ type: 'number',
274
+ description: 'Max comments (1-50, default 25).',
275
+ minimum: 1,
276
+ maximum: 50,
277
+ },
278
+ cursor: {
279
+ type: 'string',
280
+ description: 'Pagination cursor from a previous call.',
281
+ },
282
+ },
283
+ },
284
+ },
285
+ build: (ctx) => async (input) => {
286
+ const accessToken = await ctx.getAccessToken();
287
+ const postId = input.postId;
288
+ const parentCommentId = input.parentCommentId;
289
+ if (!postId && !parentCommentId) {
290
+ throw new Error('Either `postId` or `parentCommentId` must be provided.');
291
+ }
292
+ const limit = Math.min(Math.max(Number(input.limit ?? 25), 1), 50);
293
+ const path = parentCommentId
294
+ ? `/${encodeURIComponent(parentCommentId)}/comments`
295
+ : `/${encodeURIComponent(postId)}/comments`;
296
+ const result = await (0, http_1.metaGet)({
297
+ accessToken,
298
+ path,
299
+ query: {
300
+ fields: 'id,message,created_time,from{id,name},like_count,comment_count,is_hidden,parent{id}',
301
+ order: input.order ?? 'reverse_chronological',
302
+ limit,
303
+ after: input.cursor,
304
+ },
305
+ });
306
+ return JSON.stringify({
307
+ comments: result.data.map((c) => ({
308
+ id: c.id,
309
+ message: c.message,
310
+ authorId: c.from?.id,
311
+ authorName: c.from?.name,
312
+ likeCount: c.like_count ?? 0,
313
+ replyCount: c.comment_count ?? 0,
314
+ hidden: c.is_hidden ?? false,
315
+ parentId: c.parent?.id,
316
+ createdAt: c.created_time,
317
+ })),
318
+ pageInfo: {
319
+ nextCursor: result.paging?.cursors?.after,
320
+ hasNextPage: Boolean(result.paging?.next),
321
+ },
322
+ });
323
+ },
324
+ };
325
+ exports.fbReplyCommentTool = {
326
+ definition: {
327
+ name: 'fb_reply_comment',
328
+ description: "Reply to a comment on a Facebook Page post. The reply nests " +
329
+ 'under the target comment as a public child comment. Returns ' +
330
+ 'the new comment id.',
331
+ inputSchema: {
332
+ type: 'object',
333
+ properties: {
334
+ commentId: {
335
+ type: 'string',
336
+ description: 'FB comment id to reply to (from `fb_list_comments`).',
337
+ },
338
+ message: {
339
+ type: 'string',
340
+ description: 'Reply text, ≤8000 chars (FB hard limit).',
341
+ maxLength: 8000,
342
+ },
343
+ },
344
+ required: ['commentId', 'message'],
345
+ },
346
+ },
347
+ build: (ctx) => async (input) => {
348
+ const accessToken = await ctx.getAccessToken();
349
+ const commentId = String(input.commentId ?? '').trim();
350
+ const message = String(input.message ?? '').trim();
351
+ if (!commentId)
352
+ throw new Error('commentId is required.');
353
+ if (!message)
354
+ throw new Error('message is required.');
355
+ const result = await (0, http_1.metaPost)({
356
+ accessToken,
357
+ path: `/${encodeURIComponent(commentId)}/comments`,
358
+ body: { message },
359
+ });
360
+ return JSON.stringify({ commentId: result.id });
361
+ },
362
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Instagram Graph API tools.
3
+ *
4
+ * Six tools covering the MVP scope the agent needs to act as a brand
5
+ * social-media respondent:
6
+ *
7
+ * 1. ig_list_threads — inbox threads, newest first
8
+ * 2. ig_list_messages — messages in a thread
9
+ * 3. ig_send_message — send a DM (24h window applies)
10
+ * 4. ig_list_media — recent feed posts (id + caption + media URL)
11
+ * 5. ig_list_comments — comments on a specific post
12
+ * 6. ig_reply_comment — reply to a comment thread
13
+ *
14
+ * Token model:
15
+ * - `ctx.getAccessToken()` returns the PAGE access token that was
16
+ * persisted at connect-time (NOT the user token). All IG Graph
17
+ * calls accept the page token for the linked IG Business account.
18
+ * - `ctx.metadata.igUserId` is the linked IG Business Account id —
19
+ * the IG user id, not the @-handle. Set by the connect modal when
20
+ * the operator picks the Page.
21
+ *
22
+ * Why these endpoints (not the Basic Display API):
23
+ * - The Basic Display API is being deprecated and only covers PERSONAL
24
+ * IG accounts. The Graph API (what we use) covers Business +
25
+ * Creator accounts via the Page link. For an agent that needs to
26
+ * READ and REPLY, Graph API is the only viable choice.
27
+ *
28
+ * 24h messaging window:
29
+ * - Meta enforces a 24h reply window for unsolicited DMs. Sending
30
+ * after 24h returns error code 10. We surface the constraint in
31
+ * the tool description but don't pre-validate — there's no reliable
32
+ * way to check from the client (the "last user message" timestamp
33
+ * of every conversation would be N+1 calls per send), and the API
34
+ * error is the authoritative answer.
35
+ */
36
+ import type { ConnectorToolFactory } from '@agentforge-io/core';
37
+ export declare const igListThreadsTool: ConnectorToolFactory;
38
+ export declare const igListMessagesTool: ConnectorToolFactory;
39
+ export declare const igSendMessageTool: ConnectorToolFactory;
40
+ export declare const igListMediaTool: ConnectorToolFactory;
41
+ export declare const igListCommentsTool: ConnectorToolFactory;
42
+ export declare const igReplyCommentTool: ConnectorToolFactory;