@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,389 @@
1
+ "use strict";
2
+ /**
3
+ * Instagram Graph API tools.
4
+ *
5
+ * Six tools covering the MVP scope the agent needs to act as a brand
6
+ * social-media respondent:
7
+ *
8
+ * 1. ig_list_threads — inbox threads, newest first
9
+ * 2. ig_list_messages — messages in a thread
10
+ * 3. ig_send_message — send a DM (24h window applies)
11
+ * 4. ig_list_media — recent feed posts (id + caption + media URL)
12
+ * 5. ig_list_comments — comments on a specific post
13
+ * 6. ig_reply_comment — reply to a comment thread
14
+ *
15
+ * Token model:
16
+ * - `ctx.getAccessToken()` returns the PAGE access token that was
17
+ * persisted at connect-time (NOT the user token). All IG Graph
18
+ * calls accept the page token for the linked IG Business account.
19
+ * - `ctx.metadata.igUserId` is the linked IG Business Account id —
20
+ * the IG user id, not the @-handle. Set by the connect modal when
21
+ * the operator picks the Page.
22
+ *
23
+ * Why these endpoints (not the Basic Display API):
24
+ * - The Basic Display API is being deprecated and only covers PERSONAL
25
+ * IG accounts. The Graph API (what we use) covers Business +
26
+ * Creator accounts via the Page link. For an agent that needs to
27
+ * READ and REPLY, Graph API is the only viable choice.
28
+ *
29
+ * 24h messaging window:
30
+ * - Meta enforces a 24h reply window for unsolicited DMs. Sending
31
+ * after 24h returns error code 10. We surface the constraint in
32
+ * the tool description but don't pre-validate — there's no reliable
33
+ * way to check from the client (the "last user message" timestamp
34
+ * of every conversation would be N+1 calls per send), and the API
35
+ * error is the authoritative answer.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.igReplyCommentTool = exports.igListCommentsTool = exports.igListMediaTool = exports.igSendMessageTool = exports.igListMessagesTool = exports.igListThreadsTool = void 0;
39
+ const http_1 = require("../http");
40
+ const _shared_1 = require("./_shared");
41
+ exports.igListThreadsTool = {
42
+ definition: {
43
+ name: 'ig_list_threads',
44
+ description: "List the Instagram Business inbox's conversation threads, " +
45
+ 'newest-updated first. Returns thread id, updated time, ' +
46
+ 'participant ids/usernames, and the latest message preview. Use ' +
47
+ '`cursor` from a previous response to paginate.',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ limit: {
52
+ type: 'number',
53
+ description: 'Max threads (1-50, default 20).',
54
+ minimum: 1,
55
+ maximum: 50,
56
+ },
57
+ cursor: {
58
+ type: 'string',
59
+ description: 'Pagination cursor from a previous call. Omit on the first page.',
60
+ },
61
+ },
62
+ },
63
+ },
64
+ build: (ctx) => async (input) => {
65
+ const igUserId = (0, _shared_1.resolveIgUserId)(ctx);
66
+ const accessToken = await ctx.getAccessToken();
67
+ const limit = Math.min(Math.max(Number(input.limit ?? 20), 1), 50);
68
+ const result = await (0, http_1.metaGet)({
69
+ accessToken,
70
+ path: `/${encodeURIComponent(igUserId)}/conversations`,
71
+ query: {
72
+ platform: 'instagram',
73
+ fields: 'id,updated_time,participants{id,username},messages.limit(1){id,message,from{id,username},created_time}',
74
+ limit,
75
+ after: input.cursor,
76
+ },
77
+ });
78
+ return JSON.stringify({
79
+ threads: result.data.map((t) => ({
80
+ id: t.id,
81
+ updatedAt: t.updated_time,
82
+ participants: t.participants?.data?.map((p) => ({
83
+ id: p.id,
84
+ username: p.username,
85
+ })) ?? [],
86
+ latestMessage: t.messages?.data?.[0]
87
+ ? {
88
+ id: t.messages.data[0].id,
89
+ text: t.messages.data[0].message,
90
+ fromId: t.messages.data[0].from?.id,
91
+ fromUsername: t.messages.data[0].from?.username,
92
+ createdAt: t.messages.data[0].created_time,
93
+ }
94
+ : null,
95
+ })),
96
+ pageInfo: {
97
+ nextCursor: result.paging?.cursors?.after,
98
+ hasNextPage: Boolean(result.paging?.next),
99
+ },
100
+ });
101
+ },
102
+ };
103
+ exports.igListMessagesTool = {
104
+ definition: {
105
+ name: 'ig_list_messages',
106
+ description: 'List the messages in an Instagram DM thread, newest first. ' +
107
+ 'Returns message id, text, sender id/username, created time, and ' +
108
+ 'attachment URLs (image/video/audio).',
109
+ inputSchema: {
110
+ type: 'object',
111
+ properties: {
112
+ threadId: {
113
+ type: 'string',
114
+ description: 'Conversation id from `ig_list_threads`.',
115
+ },
116
+ limit: {
117
+ type: 'number',
118
+ description: 'Max messages (1-100, default 25).',
119
+ minimum: 1,
120
+ maximum: 100,
121
+ },
122
+ },
123
+ required: ['threadId'],
124
+ },
125
+ },
126
+ build: (ctx) => async (input) => {
127
+ if (!input.threadId || typeof input.threadId !== 'string') {
128
+ throw new Error('threadId is required (from ig_list_threads).');
129
+ }
130
+ const accessToken = await ctx.getAccessToken();
131
+ const limit = Math.min(Math.max(Number(input.limit ?? 25), 1), 100);
132
+ // Conversations expose their messages via the `messages` edge. We
133
+ // request the inner fields in one shot instead of doing per-message
134
+ // GETs — it costs the same quota but saves N round-trips.
135
+ const result = await (0, http_1.metaGet)({
136
+ accessToken,
137
+ path: `/${encodeURIComponent(input.threadId)}`,
138
+ query: {
139
+ fields: `messages.limit(${limit}){id,message,from{id,username},to{id,username},created_time,attachments{id,mime_type,name,file_url,image_data,video_data}}`,
140
+ },
141
+ });
142
+ return JSON.stringify({
143
+ messages: result.messages?.data?.map((m) => ({
144
+ id: m.id,
145
+ text: m.message,
146
+ fromId: m.from?.id,
147
+ fromUsername: m.from?.username,
148
+ toIds: m.to?.data?.map((t) => t.id),
149
+ createdAt: m.created_time,
150
+ attachments: m.attachments?.data?.map((a) => ({
151
+ id: a.id,
152
+ mimeType: a.mime_type,
153
+ name: a.name,
154
+ url: a.file_url ?? a.image_data?.url ?? a.video_data?.url,
155
+ })) ?? [],
156
+ })) ?? [],
157
+ });
158
+ },
159
+ };
160
+ exports.igSendMessageTool = {
161
+ definition: {
162
+ name: 'ig_send_message',
163
+ description: 'Send a DM to a user on Instagram. The recipient must have ' +
164
+ 'messaged the connected IG account within the last 24 hours; ' +
165
+ 'outside that window Meta returns error code 10 and the message ' +
166
+ "is not delivered. Pass either `recipientId` (the IG user id you " +
167
+ 'got from `ig_list_threads` participants) or `threadId` ' +
168
+ '(conversation id) — passing both is redundant.',
169
+ inputSchema: {
170
+ type: 'object',
171
+ properties: {
172
+ recipientId: {
173
+ type: 'string',
174
+ description: 'IG user id of the recipient (from `ig_list_threads` ' +
175
+ 'participants[].id). Required when `threadId` is not set.',
176
+ },
177
+ threadId: {
178
+ type: 'string',
179
+ description: 'Conversation id (from `ig_list_threads`). When set, the ' +
180
+ 'message goes to the same thread without needing the ' +
181
+ 'recipient id.',
182
+ },
183
+ text: {
184
+ type: 'string',
185
+ description: 'Message body, ≤1000 chars (Meta hard limit).',
186
+ maxLength: 1000,
187
+ },
188
+ },
189
+ required: ['text'],
190
+ },
191
+ },
192
+ build: (ctx) => async (input) => {
193
+ const igUserId = (0, _shared_1.resolveIgUserId)(ctx);
194
+ const accessToken = await ctx.getAccessToken();
195
+ const text = String(input.text ?? '').trim();
196
+ if (!text) {
197
+ throw new Error('text is required and must not be empty.');
198
+ }
199
+ const recipientId = input.recipientId;
200
+ const threadId = input.threadId;
201
+ if (!recipientId && !threadId) {
202
+ throw new Error('Either `recipientId` or `threadId` must be provided.');
203
+ }
204
+ // Meta accepts EITHER `recipient.id` (a user id) OR
205
+ // `recipient.thread_key` (a conversation id). We prefer thread_key
206
+ // when both are provided because it's more robust against IG users
207
+ // changing their ig-scoped id across re-authentications.
208
+ const recipient = threadId
209
+ ? { thread_key: threadId }
210
+ : { id: recipientId };
211
+ const result = await (0, http_1.metaPost)({
212
+ accessToken,
213
+ path: `/${encodeURIComponent(igUserId)}/messages`,
214
+ body: {
215
+ recipient,
216
+ message: { text },
217
+ },
218
+ });
219
+ return JSON.stringify({
220
+ messageId: result.message_id,
221
+ recipientId: result.recipient_id,
222
+ });
223
+ },
224
+ };
225
+ exports.igListMediaTool = {
226
+ definition: {
227
+ name: 'ig_list_media',
228
+ description: "List the connected Instagram Business account's published " +
229
+ 'media (feed posts, reels, carousels), newest first. Returns id, ' +
230
+ 'caption, media type, media URL, permalink, and engagement ' +
231
+ 'counts. Use the post id with `ig_list_comments` to read replies.',
232
+ inputSchema: {
233
+ type: 'object',
234
+ properties: {
235
+ limit: {
236
+ type: 'number',
237
+ description: 'Max items (1-50, default 20).',
238
+ minimum: 1,
239
+ maximum: 50,
240
+ },
241
+ cursor: {
242
+ type: 'string',
243
+ description: 'Pagination cursor from a previous call.',
244
+ },
245
+ },
246
+ },
247
+ },
248
+ build: (ctx) => async (input) => {
249
+ const igUserId = (0, _shared_1.resolveIgUserId)(ctx);
250
+ const accessToken = await ctx.getAccessToken();
251
+ const limit = Math.min(Math.max(Number(input.limit ?? 20), 1), 50);
252
+ const result = await (0, http_1.metaGet)({
253
+ accessToken,
254
+ path: `/${encodeURIComponent(igUserId)}/media`,
255
+ query: {
256
+ fields: 'id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,comments_count,like_count',
257
+ limit,
258
+ after: input.cursor,
259
+ },
260
+ });
261
+ return JSON.stringify({
262
+ media: result.data.map((m) => ({
263
+ id: m.id,
264
+ caption: m.caption,
265
+ mediaType: m.media_type,
266
+ mediaUrl: m.media_url,
267
+ permalink: m.permalink,
268
+ thumbnailUrl: m.thumbnail_url,
269
+ publishedAt: m.timestamp,
270
+ commentsCount: m.comments_count,
271
+ likeCount: m.like_count,
272
+ })),
273
+ pageInfo: {
274
+ nextCursor: result.paging?.cursors?.after,
275
+ hasNextPage: Boolean(result.paging?.next),
276
+ },
277
+ });
278
+ },
279
+ };
280
+ exports.igListCommentsTool = {
281
+ definition: {
282
+ name: 'ig_list_comments',
283
+ description: 'List comments on an Instagram media item (or replies under a ' +
284
+ 'specific parent comment). Returns comment id, text, author ' +
285
+ 'username, like count, hidden flag, and a reply-count hint.',
286
+ inputSchema: {
287
+ type: 'object',
288
+ properties: {
289
+ mediaId: {
290
+ type: 'string',
291
+ description: 'IG media id from `ig_list_media`. Required when ' +
292
+ '`parentCommentId` is not set.',
293
+ },
294
+ parentCommentId: {
295
+ type: 'string',
296
+ description: "Comment id to read replies under. When set, mediaId is " +
297
+ 'ignored — the API routes through the parent comment.',
298
+ },
299
+ limit: {
300
+ type: 'number',
301
+ description: 'Max comments (1-50, default 25).',
302
+ minimum: 1,
303
+ maximum: 50,
304
+ },
305
+ cursor: {
306
+ type: 'string',
307
+ description: 'Pagination cursor from a previous call.',
308
+ },
309
+ },
310
+ },
311
+ },
312
+ build: (ctx) => async (input) => {
313
+ const accessToken = await ctx.getAccessToken();
314
+ const mediaId = input.mediaId;
315
+ const parentCommentId = input.parentCommentId;
316
+ if (!mediaId && !parentCommentId) {
317
+ throw new Error('Either `mediaId` or `parentCommentId` must be provided.');
318
+ }
319
+ const limit = Math.min(Math.max(Number(input.limit ?? 25), 1), 50);
320
+ const path = parentCommentId
321
+ ? `/${encodeURIComponent(parentCommentId)}/replies`
322
+ : `/${encodeURIComponent(mediaId)}/comments`;
323
+ const result = await (0, http_1.metaGet)({
324
+ accessToken,
325
+ path,
326
+ query: {
327
+ fields: 'id,text,timestamp,username,like_count,hidden,replies.limit(0)',
328
+ limit,
329
+ after: input.cursor,
330
+ },
331
+ });
332
+ return JSON.stringify({
333
+ comments: result.data.map((c) => ({
334
+ id: c.id,
335
+ text: c.text,
336
+ username: c.username,
337
+ likeCount: c.like_count,
338
+ hidden: c.hidden,
339
+ // `replies.limit(0)` returns the paging envelope without rows.
340
+ // We use the presence of the edge to expose "this comment has
341
+ // replies" without paying for them.
342
+ hasReplies: Boolean(c.replies?.data?.length),
343
+ createdAt: c.timestamp,
344
+ })),
345
+ pageInfo: {
346
+ nextCursor: result.paging?.cursors?.after,
347
+ hasNextPage: Boolean(result.paging?.next),
348
+ },
349
+ });
350
+ },
351
+ };
352
+ exports.igReplyCommentTool = {
353
+ definition: {
354
+ name: 'ig_reply_comment',
355
+ description: 'Reply to a comment on an Instagram post. The reply nests under ' +
356
+ 'the target comment as a public child reply. Returns the new ' +
357
+ 'comment id.',
358
+ inputSchema: {
359
+ type: 'object',
360
+ properties: {
361
+ commentId: {
362
+ type: 'string',
363
+ description: 'IG comment id to reply to (from `ig_list_comments`).',
364
+ },
365
+ message: {
366
+ type: 'string',
367
+ description: 'Reply text, ≤2200 chars (IG hard limit).',
368
+ maxLength: 2200,
369
+ },
370
+ },
371
+ required: ['commentId', 'message'],
372
+ },
373
+ },
374
+ build: (ctx) => async (input) => {
375
+ const accessToken = await ctx.getAccessToken();
376
+ const commentId = String(input.commentId ?? '').trim();
377
+ const message = String(input.message ?? '').trim();
378
+ if (!commentId)
379
+ throw new Error('commentId is required.');
380
+ if (!message)
381
+ throw new Error('message is required.');
382
+ const result = await (0, http_1.metaPost)({
383
+ accessToken,
384
+ path: `/${encodeURIComponent(commentId)}/replies`,
385
+ body: { message },
386
+ });
387
+ return JSON.stringify({ commentId: result.id });
388
+ },
389
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Messenger (Page DMs) tools.
3
+ *
4
+ * Four tools covering the MVP scope for a Page-bound respondent:
5
+ *
6
+ * 1. messenger_list_threads — Page inbox, newest first
7
+ * 2. messenger_list_messages — messages in a thread
8
+ * 3. messenger_send_message — send a message (24h window + tags)
9
+ * 4. messenger_mark_seen — sender_action: mark thread as seen
10
+ *
11
+ * Token model:
12
+ * - `ctx.getAccessToken()` returns the PAGE access token persisted at
13
+ * connect-time. Same property as Instagram — the page token
14
+ * authorizes both surfaces.
15
+ * - `ctx.metadata.pageId` is the Page id (NOT the IG user id). Used
16
+ * in every endpoint path here.
17
+ *
18
+ * Why so close to Instagram's shape (but a separate file):
19
+ * - Conversations API is shared (same fields), so the response
20
+ * parsing is similar. But the routing key is different (page id vs
21
+ * IG user id), the platform discriminator is different, and the
22
+ * send-message body has a Messenger-specific `messaging_type` +
23
+ * `tag` set that doesn't exist on Instagram. Trying to share a
24
+ * single send-tool implementation between them would mean a giant
25
+ * conditional on platform; cleaner to fork.
26
+ *
27
+ * 24h messaging window (the Messenger-specific quirk):
28
+ * - Inside 24h of the user's last message → `messaging_type=RESPONSE`
29
+ * (free-form, no tag).
30
+ * - Outside 24h → `messaging_type=MESSAGE_TAG` + an approved `tag`
31
+ * (HUMAN_AGENT, ACCOUNT_UPDATE, CONFIRMED_EVENT_UPDATE,
32
+ * POST_PURCHASE_UPDATE). Sending without a tag returns error
33
+ * code 10 ("App does not have permission for this messaging").
34
+ * - We expose `messagingType` + `tag` in the schema; defaults are
35
+ * RESPONSE/null. The agent can pick MESSAGE_TAG when it knows it's
36
+ * outside the window — the API is authoritative on the actual
37
+ * decision.
38
+ */
39
+ import type { ConnectorToolFactory } from '@agentforge-io/core';
40
+ export declare const messengerListThreadsTool: ConnectorToolFactory;
41
+ export declare const messengerListMessagesTool: ConnectorToolFactory;
42
+ export declare const messengerSendMessageTool: ConnectorToolFactory;
43
+ export declare const messengerMarkSeenTool: ConnectorToolFactory;