@jclvsh/iris 0.20.0 → 0.22.1
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 +33 -0
- package/dist/cli.js +2 -0
- package/dist/commands/campaigns.js +1 -1
- package/dist/commands/responder.d.ts +3 -0
- package/dist/commands/responder.js +261 -0
- package/dist/commands/webhooks.js +1 -1
- package/dist/index.js +2 -0
- package/dist/tools/campaigns.js +1 -1
- package/dist/tools/responder.d.ts +3 -0
- package/dist/tools/responder.js +219 -0
- package/dist/tools/webhooks.js +2 -0
- package/dist/types.d.ts +102 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ iris campaigns --help
|
|
|
74
74
|
| billing | `b` | plans | 1 |
|
|
75
75
|
| channels | `ch` | accounts, availability, send | 3 |
|
|
76
76
|
| credits | `cr` | balance, buy | 2 |
|
|
77
|
+
| responder | `r` | status, configure, activity, show, pause, resume, triggers list, triggers add, triggers update, triggers remove, triggers posts | 11 |
|
|
77
78
|
|
|
78
79
|
## MCP Server Configuration
|
|
79
80
|
|
|
@@ -351,6 +352,36 @@ Add to `.cursor/mcp.json`:
|
|
|
351
352
|
| `get_credit_balance` | Check the agent credit balance |
|
|
352
353
|
| `purchase_credit_pack` | Purchase a 5-credit pack via MPP ($12.00) |
|
|
353
354
|
|
|
355
|
+
### Responder
|
|
356
|
+
|
|
357
|
+
| Tool | Description |
|
|
358
|
+
| ---------------------------------- | ------------------------------------------------------------------------------- |
|
|
359
|
+
| `get_responder` | Get the config, whether iris is replying right now, and last-30-days counts |
|
|
360
|
+
| `configure_responder` | Create or update the responder config |
|
|
361
|
+
| `list_responder_activity` | List what the responder did (replies sent, handoffs, skips, failures) |
|
|
362
|
+
| `get_conversation_responder` | Get the responder's state for a single conversation |
|
|
363
|
+
| `set_conversation_responder` | Pause or resume the responder for a single conversation |
|
|
364
|
+
| `list_responder_comment_triggers` | List comment triggers |
|
|
365
|
+
| `create_responder_comment_trigger` | Create a comment trigger |
|
|
366
|
+
| `update_responder_comment_trigger` | Update a comment trigger's keyword, message, post, or enabled state |
|
|
367
|
+
| `delete_responder_comment_trigger` | Delete a comment trigger |
|
|
368
|
+
| `list_responder_posts` | List the newest Instagram posts and their media ids, for a trigger's `media_id` |
|
|
369
|
+
|
|
370
|
+
The responder answers inbound Instagram DMs for the account owner. It is off by default. It needs an API key and a connected Instagram account, so agents paying by x402 or MPP cannot use it.
|
|
371
|
+
|
|
372
|
+
- Replies go only to someone who wrote first.
|
|
373
|
+
- A reply goes out only within 24 hours of their last message.
|
|
374
|
+
- It cannot read photos, videos, or story replies.
|
|
375
|
+
- It cannot see whether someone booked.
|
|
376
|
+
|
|
377
|
+
A comment trigger watches for a keyword in the comments on the account's Instagram posts. When someone comments it, iris sends that person one direct message: the owner's message as written, followed by the notice that it is automated. Instagram allows one such message per comment, within 7 days of the comment, and nothing more unless the person replies.
|
|
378
|
+
|
|
379
|
+
- It sends only while the responder is enabled and not paused, the subscription is active, and `status.comment_triggers` on `get_responder` is `ready`.
|
|
380
|
+
- `needs_permission` means the owner has to allow comments on the Instagram connection, from the responder settings page in the iris web app. `unavailable` means comment triggers are not offered on the account yet, and their tools return a 403.
|
|
381
|
+
- A trigger applies to every post, or to one post named by `media_id`. `list_responder_posts` lists the newest posts with their ids.
|
|
382
|
+
- Each message sent counts as a message against the plan and shares the account's send pacing. A comment gets no message when it arrives while that pace or the plan's message allowance is reached, or when Instagram refuses or fails the send. iris does not try the same comment again.
|
|
383
|
+
- An account holds up to 20 triggers.
|
|
384
|
+
|
|
354
385
|
## API Key & Scopes
|
|
355
386
|
|
|
356
387
|
Generate an API key at [iris-ai.dev/settings/api-keys](https://iris-ai.dev/settings/api-keys).
|
|
@@ -401,6 +432,8 @@ API keys support fine-grained scopes per resource:
|
|
|
401
432
|
| `message.rejected` | Message rejected in the queue |
|
|
402
433
|
| `discovery.completed` | Discovery job finished successfully |
|
|
403
434
|
| `discovery.failed` | Discovery job failed |
|
|
435
|
+
| `responder.reply_sent` | Responder sent an automatic reply |
|
|
436
|
+
| `responder.handoff` | Responder handed a conversation off |
|
|
404
437
|
|
|
405
438
|
Webhook deliveries are signed with HMAC-SHA256 using the secret returned from `create_webhook` / `rotate_webhook_secret`. Verify the signature in the `X-Iris-Signature` header before trusting the payload.
|
|
406
439
|
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerAnalyticsCommands } from "./commands/analytics.js";
|
|
|
17
17
|
import { registerBillingCommands } from "./commands/billing.js";
|
|
18
18
|
import { registerChannelCommands } from "./commands/channels.js";
|
|
19
19
|
import { registerCreditCommands } from "./commands/credits.js";
|
|
20
|
+
import { registerResponderCommands } from "./commands/responder.js";
|
|
20
21
|
const program = new Command()
|
|
21
22
|
.name("iris")
|
|
22
23
|
.description("iris CLI - manage campaigns, contacts, messages, and outreach")
|
|
@@ -50,6 +51,7 @@ try {
|
|
|
50
51
|
registerBillingCommands(program, client);
|
|
51
52
|
registerChannelCommands(program, client);
|
|
52
53
|
registerCreditCommands(program, client);
|
|
54
|
+
registerResponderCommands(program, client);
|
|
53
55
|
await program.parseAsync(process.argv);
|
|
54
56
|
}
|
|
55
57
|
catch (error) {
|
|
@@ -110,7 +110,7 @@ export function registerCampaignCommands(program, client) {
|
|
|
110
110
|
});
|
|
111
111
|
campaigns
|
|
112
112
|
.command("analyze <id>")
|
|
113
|
-
.description("
|
|
113
|
+
.description("queue AI analysis of a campaign to generate target profiles (from its product, or from its goal)")
|
|
114
114
|
.action(async (id) => {
|
|
115
115
|
try {
|
|
116
116
|
const result = await client.post(`/campaigns/${id}/analyze`);
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { formatOutput, handleError, getFormat, parseList } from "../cli-utils.js";
|
|
2
|
+
const CLEARABLE_FIELDS = ["instructions", "booking-url", "disclosure"];
|
|
3
|
+
function parseStrictBoolean(value, flag) {
|
|
4
|
+
if (value === "true")
|
|
5
|
+
return true;
|
|
6
|
+
if (value === "false")
|
|
7
|
+
return false;
|
|
8
|
+
throw new Error(`invalid value for ${flag}: "${value}" (expected "true" or "false")`);
|
|
9
|
+
}
|
|
10
|
+
function collectQuestion(value, previous) {
|
|
11
|
+
if (previous) {
|
|
12
|
+
previous.push(value);
|
|
13
|
+
return previous;
|
|
14
|
+
}
|
|
15
|
+
return [value];
|
|
16
|
+
}
|
|
17
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
+
/** The id goes into the request path, so it has to be an id and nothing else. */
|
|
19
|
+
function requireTriggerId(id) {
|
|
20
|
+
if (UUID.test(id))
|
|
21
|
+
return;
|
|
22
|
+
console.error("error: the comment trigger id must be a uuid (see `responder triggers list`)");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
export function registerResponderCommands(program, client) {
|
|
26
|
+
const responder = program.command("responder").alias("r").description("manage the Instagram DM auto-responder");
|
|
27
|
+
responder
|
|
28
|
+
.command("status")
|
|
29
|
+
.description("get responder status: configured, current config, connection/pause status, and last-30-days activity")
|
|
30
|
+
.action(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.get("/responder");
|
|
33
|
+
formatOutput(result, getFormat(program.opts()));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
handleError(error);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
responder
|
|
40
|
+
.command("configure")
|
|
41
|
+
.description("create or update the responder config (merge: only the options you pass change)")
|
|
42
|
+
.option("--enabled <bool>", '"true" or "false" - thread Instagram DMs into the inbox')
|
|
43
|
+
.option("--auto-reply <bool>", '"true" or "false" - let iris send replies without review')
|
|
44
|
+
.option("--instructions <text>", "what you offer, prices, answers to common questions, tone")
|
|
45
|
+
.option("--booking-url <url>", "https url shared when someone asks to talk or book")
|
|
46
|
+
.option("--question <text>", "a qualifying question, asked in order before the booking link (repeatable)", collectQuestion)
|
|
47
|
+
.option("--clear-questions", "clear all qualifying questions (sends an empty array)")
|
|
48
|
+
.option("--disclosure <text>", "the automated-reply disclosure line (10-200 chars)")
|
|
49
|
+
.option("--max-replies <n>", "automatic replies per conversation before handoff (1-30, default 8)")
|
|
50
|
+
.option("--clear <fields>", `comma-separated fields to unset: ${CLEARABLE_FIELDS.join(", ")}`, parseList)
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
try {
|
|
53
|
+
const body = {};
|
|
54
|
+
if (opts.clear) {
|
|
55
|
+
for (const field of opts.clear) {
|
|
56
|
+
if (!CLEARABLE_FIELDS.includes(field)) {
|
|
57
|
+
console.error(`error: unknown --clear field "${field}" (expected one of: ${CLEARABLE_FIELDS.join(", ")})`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (opts.clear.includes("instructions"))
|
|
62
|
+
body.instructions = null;
|
|
63
|
+
if (opts.clear.includes("booking-url"))
|
|
64
|
+
body.booking_url = null;
|
|
65
|
+
if (opts.clear.includes("disclosure"))
|
|
66
|
+
body.disclosure = null;
|
|
67
|
+
}
|
|
68
|
+
if (opts.enabled !== undefined)
|
|
69
|
+
body.enabled = parseStrictBoolean(opts.enabled, "--enabled");
|
|
70
|
+
if (opts.autoReply !== undefined)
|
|
71
|
+
body.auto_reply = parseStrictBoolean(opts.autoReply, "--auto-reply");
|
|
72
|
+
if (opts.instructions !== undefined)
|
|
73
|
+
body.instructions = opts.instructions;
|
|
74
|
+
if (opts.bookingUrl !== undefined)
|
|
75
|
+
body.booking_url = opts.bookingUrl;
|
|
76
|
+
if (opts.clearQuestions)
|
|
77
|
+
body.qualifying_questions = [];
|
|
78
|
+
else if (opts.question !== undefined)
|
|
79
|
+
body.qualifying_questions = opts.question;
|
|
80
|
+
if (opts.disclosure !== undefined)
|
|
81
|
+
body.disclosure = opts.disclosure;
|
|
82
|
+
if (opts.maxReplies !== undefined) {
|
|
83
|
+
const maxReplies = Number(opts.maxReplies);
|
|
84
|
+
if (!Number.isInteger(maxReplies) || maxReplies < 1 || maxReplies > 30) {
|
|
85
|
+
throw new Error(`invalid value for --max-replies: "${opts.maxReplies}" (expected a whole number from 1 to 30)`);
|
|
86
|
+
}
|
|
87
|
+
body.max_replies_per_conversation = maxReplies;
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(body).length === 0) {
|
|
90
|
+
console.error("error: at least one option is required");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const result = await client.patch("/responder", body);
|
|
94
|
+
formatOutput(result, getFormat(program.opts()));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
handleError(error);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
responder
|
|
101
|
+
.command("activity")
|
|
102
|
+
.description("list the responder's activity log (replies sent, handoffs, skips, failures)")
|
|
103
|
+
.option("-p, --page <n>", "page number", "1")
|
|
104
|
+
.option("-s, --page-size <n>", "items per page (max 100)", "50")
|
|
105
|
+
.action(async (opts) => {
|
|
106
|
+
try {
|
|
107
|
+
const params = {};
|
|
108
|
+
if (opts.page)
|
|
109
|
+
params.page = opts.page;
|
|
110
|
+
if (opts.pageSize)
|
|
111
|
+
params.page_size = opts.pageSize;
|
|
112
|
+
const result = await client.get("/responder/activity", params);
|
|
113
|
+
formatOutput(result, getFormat(program.opts()));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
handleError(error);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
responder
|
|
120
|
+
.command("show <conversation-id>")
|
|
121
|
+
.description("get the responder's status for a single conversation")
|
|
122
|
+
.action(async (conversationId) => {
|
|
123
|
+
try {
|
|
124
|
+
const result = await client.get(`/conversations/${conversationId}/responder`);
|
|
125
|
+
formatOutput(result, getFormat(program.opts()));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
handleError(error);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
responder
|
|
132
|
+
.command("pause <conversation-id>")
|
|
133
|
+
.description("pause the responder for a single conversation")
|
|
134
|
+
.action(async (conversationId) => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await client.patch(`/conversations/${conversationId}/responder`, { status: "paused" });
|
|
137
|
+
formatOutput(result, getFormat(program.opts()));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
handleError(error);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
responder
|
|
144
|
+
.command("resume <conversation-id>")
|
|
145
|
+
.description("resume the responder for a single conversation (refused if the person asked iris to stop)")
|
|
146
|
+
.action(async (conversationId) => {
|
|
147
|
+
try {
|
|
148
|
+
const result = await client.patch(`/conversations/${conversationId}/responder`, { status: "active" });
|
|
149
|
+
formatOutput(result, getFormat(program.opts()));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
handleError(error);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const triggers = responder
|
|
156
|
+
.command("triggers")
|
|
157
|
+
.alias("tr")
|
|
158
|
+
.description("manage comment triggers: a keyword in an instagram comment that sends the commenter one automatic DM");
|
|
159
|
+
triggers
|
|
160
|
+
.command("list")
|
|
161
|
+
.description("list comment triggers")
|
|
162
|
+
.action(async () => {
|
|
163
|
+
try {
|
|
164
|
+
const result = await client.get("/responder/comment-triggers");
|
|
165
|
+
formatOutput(result, getFormat(program.opts()));
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
handleError(error);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
triggers
|
|
172
|
+
.command("add")
|
|
173
|
+
.description("create a comment trigger")
|
|
174
|
+
.requiredOption("-k, --keyword <text>", "matched as a whole word or phrase in the comment, case-insensitive (1-50 characters)")
|
|
175
|
+
.requiredOption("-m, --message <text>", "the DM sent to the commenter, before the automated notice (1-900 characters, at most 700 bytes)")
|
|
176
|
+
.option("--media-id <id>", "the numeric instagram media id to limit the trigger to one post (5-40 digits, from `responder triggers posts`); omit to match every post")
|
|
177
|
+
.option("--disabled", "create the trigger turned off (default enabled)")
|
|
178
|
+
.action(async (opts) => {
|
|
179
|
+
try {
|
|
180
|
+
const body = { keyword: opts.keyword, message: opts.message };
|
|
181
|
+
if (opts.mediaId)
|
|
182
|
+
body.media_id = opts.mediaId;
|
|
183
|
+
if (opts.disabled)
|
|
184
|
+
body.enabled = false;
|
|
185
|
+
const result = await client.post("/responder/comment-triggers", body);
|
|
186
|
+
formatOutput(result, getFormat(program.opts()));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
handleError(error);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
triggers
|
|
193
|
+
.command("update <id>")
|
|
194
|
+
.description("update a comment trigger")
|
|
195
|
+
.option("-k, --keyword <text>", "new keyword, matched case-insensitively (1-50 characters)")
|
|
196
|
+
.option("-m, --message <text>", "new DM text (1-900 characters, at most 700 bytes)")
|
|
197
|
+
.option("--media-id <id>", "limit the trigger to one post (5-40 digits, from `responder triggers posts`)")
|
|
198
|
+
.option("--all-posts", "match every post (clears media_id)")
|
|
199
|
+
.option("--enable", "turn the trigger on")
|
|
200
|
+
.option("--disable", "turn the trigger off")
|
|
201
|
+
.action(async (id, opts) => {
|
|
202
|
+
try {
|
|
203
|
+
requireTriggerId(id);
|
|
204
|
+
if (opts.mediaId && opts.allPosts) {
|
|
205
|
+
console.error("error: --media-id and --all-posts cannot both be given");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
if (opts.enable && opts.disable) {
|
|
209
|
+
console.error("error: --enable and --disable cannot both be given");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const body = {};
|
|
213
|
+
if (opts.keyword !== undefined)
|
|
214
|
+
body.keyword = opts.keyword;
|
|
215
|
+
if (opts.message !== undefined)
|
|
216
|
+
body.message = opts.message;
|
|
217
|
+
if (opts.mediaId !== undefined)
|
|
218
|
+
body.media_id = opts.mediaId;
|
|
219
|
+
if (opts.allPosts)
|
|
220
|
+
body.media_id = null;
|
|
221
|
+
if (opts.enable)
|
|
222
|
+
body.enabled = true;
|
|
223
|
+
if (opts.disable)
|
|
224
|
+
body.enabled = false;
|
|
225
|
+
if (Object.keys(body).length === 0) {
|
|
226
|
+
console.error("error: at least one option is required");
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const result = await client.patch(`/responder/comment-triggers/${id}`, body);
|
|
230
|
+
formatOutput(result, getFormat(program.opts()));
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
handleError(error);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
triggers
|
|
237
|
+
.command("remove <id>")
|
|
238
|
+
.description("delete a comment trigger")
|
|
239
|
+
.action(async (id) => {
|
|
240
|
+
try {
|
|
241
|
+
requireTriggerId(id);
|
|
242
|
+
await client.delete(`/responder/comment-triggers/${id}`);
|
|
243
|
+
console.log("comment trigger deleted");
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
handleError(error);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
triggers
|
|
250
|
+
.command("posts")
|
|
251
|
+
.description("list the account's recent instagram posts with their media ids, to find one for --media-id")
|
|
252
|
+
.action(async () => {
|
|
253
|
+
try {
|
|
254
|
+
const result = await client.get("/responder/posts");
|
|
255
|
+
formatOutput(result, getFormat(program.opts()));
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
handleError(error);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
@@ -24,7 +24,7 @@ export function registerWebhookCommands(program, client) {
|
|
|
24
24
|
.command("create")
|
|
25
25
|
.description("create a webhook endpoint")
|
|
26
26
|
.requiredOption("-u, --url <url>", "HTTPS webhook URL")
|
|
27
|
-
.requiredOption("-e, --events <list>", "comma-separated events: campaign.created, campaign.updated, campaign.deleted, contact.created, contact.updated, contact.deleted, conversation.created, conversation.reply_received, message.sent, message.delivered, message.opened, message.bounced, message.approved, message.rejected, discovery.completed, discovery.failed (or '*' for all)", parseList)
|
|
27
|
+
.requiredOption("-e, --events <list>", "comma-separated events: campaign.created, campaign.updated, campaign.deleted, contact.created, contact.updated, contact.deleted, conversation.created, conversation.reply_received, message.sent, message.delivered, message.opened, message.bounced, message.approved, message.rejected, discovery.completed, discovery.failed, responder.reply_sent, responder.handoff (or '*' for all)", parseList)
|
|
28
28
|
.option("-d, --description <text>", "optional description")
|
|
29
29
|
.action(async (opts) => {
|
|
30
30
|
try {
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { registerWebhookTools } from "./tools/webhooks.js";
|
|
|
18
18
|
import { registerAgentCreditTools } from "./tools/agent-credits.js";
|
|
19
19
|
import { registerUsageTools } from "./tools/usage.js";
|
|
20
20
|
import { registerAutopilotTools } from "./tools/autopilot.js";
|
|
21
|
+
import { registerResponderTools } from "./tools/responder.js";
|
|
21
22
|
import { registerAudienceTools } from "./tools/audiences.js";
|
|
22
23
|
const requireFromHere = createRequire(import.meta.url);
|
|
23
24
|
const pkg = requireFromHere("../package.json");
|
|
@@ -54,6 +55,7 @@ registerWebhookTools(server, client);
|
|
|
54
55
|
registerAgentCreditTools(server, client);
|
|
55
56
|
registerUsageTools(server, client);
|
|
56
57
|
registerAutopilotTools(server, client);
|
|
58
|
+
registerResponderTools(server, client);
|
|
57
59
|
registerAudienceTools(server, client);
|
|
58
60
|
const transport = new StdioServerTransport();
|
|
59
61
|
await server.connect(transport);
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -193,7 +193,7 @@ export function registerCampaignTools(server, client) {
|
|
|
193
193
|
return formatMcpError(error);
|
|
194
194
|
}
|
|
195
195
|
});
|
|
196
|
-
server.tool("analyze_campaign", "
|
|
196
|
+
server.tool("analyze_campaign", "queue AI analysis of a campaign to generate its target profiles: from the product for product_promotion campaigns, from goal_description for any other campaign. runs asynchronously; poll get_campaign until analysis_status is completed, then search_campaign_contacts. requires write scope.", {
|
|
197
197
|
id: z.string().uuid().describe("campaign ID"),
|
|
198
198
|
}, async ({ id }) => {
|
|
199
199
|
try {
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatMcpError } from "../cli-utils.js";
|
|
3
|
+
const MEDIA_ID_PATTERN = /^\d{5,40}$/;
|
|
4
|
+
function messageByteLength(value) {
|
|
5
|
+
return Buffer.byteLength(value, "utf8");
|
|
6
|
+
}
|
|
7
|
+
export function registerResponderTools(server, client) {
|
|
8
|
+
server.tool("get_responder", "get responder status: whether it's configured, the current config, connection and pause status, and activity counts for the last 30 days. requires read scope.", {}, async () => {
|
|
9
|
+
try {
|
|
10
|
+
const result = await client.get("/responder");
|
|
11
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return formatMcpError(error);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
server.tool("configure_responder", "create or update the responder config (merge: only the fields you send change; the first call creates the config). with auto_reply on, replies are sent without review. Instagram only allows a reply within 24 hours of the person's last message, and only to someone who wrote first. requires write scope.", {
|
|
18
|
+
enabled: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("thread Instagram DMs from anyone into the inbox. turning it off also turns auto_reply off. true clears any pause state."),
|
|
22
|
+
auto_reply: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("let iris send replies itself, with no review. requires enabled, and either instructions or a profile background to answer from."),
|
|
26
|
+
instructions: z
|
|
27
|
+
.string()
|
|
28
|
+
.max(4000)
|
|
29
|
+
.nullable()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("what you offer, prices, answers to common questions, tone; null to unset"),
|
|
32
|
+
booking_url: z
|
|
33
|
+
.string()
|
|
34
|
+
.url()
|
|
35
|
+
.max(500)
|
|
36
|
+
.nullable()
|
|
37
|
+
.optional()
|
|
38
|
+
.refine((value) => value === undefined || value === null || value.startsWith("https://"), {
|
|
39
|
+
message: "booking_url must use HTTPS",
|
|
40
|
+
})
|
|
41
|
+
.describe("shared when someone asks to talk or book (https url); null to unset"),
|
|
42
|
+
qualifying_questions: z
|
|
43
|
+
.array(z.string().max(200))
|
|
44
|
+
.max(5)
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("asked one per message, in order, before the booking link is shared (max 5 items)"),
|
|
47
|
+
disclosure: z
|
|
48
|
+
.string()
|
|
49
|
+
.min(10)
|
|
50
|
+
.max(200)
|
|
51
|
+
.nullable()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("the line that says the reply is automated, sent once per conversation with the first reply; null uses the default"),
|
|
54
|
+
max_replies_per_conversation: z
|
|
55
|
+
.number()
|
|
56
|
+
.int()
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(30)
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("after this many automatic replies the conversation is handed to you (default 8)"),
|
|
61
|
+
}, async (args) => {
|
|
62
|
+
try {
|
|
63
|
+
const body = Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined));
|
|
64
|
+
const result = await client.patch("/responder", body);
|
|
65
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return formatMcpError(error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
server.tool("list_responder_activity", "list the responder's activity log (replies sent, handoffs, skips, and failures), newest first. requires read scope.", {
|
|
72
|
+
page: z.number().int().positive().optional().describe("page number (default 1)"),
|
|
73
|
+
page_size: z.number().int().min(1).max(100).optional().describe("items per page (default 50, max 100)"),
|
|
74
|
+
}, async ({ page, page_size }) => {
|
|
75
|
+
try {
|
|
76
|
+
const params = {};
|
|
77
|
+
if (page !== undefined)
|
|
78
|
+
params.page = String(page);
|
|
79
|
+
if (page_size !== undefined)
|
|
80
|
+
params.page_size = String(page_size);
|
|
81
|
+
const result = await client.get("/responder/activity", params);
|
|
82
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return formatMcpError(error);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
server.tool("get_conversation_responder", "get the responder's status for a single conversation: active, paused, handed off, or never seen, plus the reason and reply counts. requires read scope.", {
|
|
89
|
+
conversation_id: z.string().uuid().describe("conversation ID"),
|
|
90
|
+
}, async ({ conversation_id }) => {
|
|
91
|
+
try {
|
|
92
|
+
const result = await client.get(`/conversations/${conversation_id}/responder`);
|
|
93
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return formatMcpError(error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.tool("set_conversation_responder", "pause or resume the responder for a single conversation. pausing works on any conversation; resuming is refused after the person asked iris to stop. requires write scope.", {
|
|
100
|
+
conversation_id: z.string().uuid().describe("conversation ID"),
|
|
101
|
+
status: z.enum(["active", "paused"]).describe("new responder status for this conversation"),
|
|
102
|
+
}, async ({ conversation_id, status }) => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await client.patch(`/conversations/${conversation_id}/responder`, {
|
|
105
|
+
status,
|
|
106
|
+
});
|
|
107
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return formatMcpError(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
server.tool("list_responder_comment_triggers", "list the account's comment triggers. a comment trigger watches for a keyword in instagram comments and sends the commenter one automatic DM. requires read scope.", {}, async () => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await client.get("/responder/comment-triggers");
|
|
116
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return formatMcpError(error);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
server.tool("create_responder_comment_trigger", "create a comment trigger. when someone comments the keyword on the account's instagram post, iris sends that person one direct message: the owner's message as written, followed by the notice that it is automated. instagram allows one such message per comment, within 7 days of the comment, and nothing more unless the person replies. it only sends while status.comment_triggers on get_responder is ready; needs_permission means the owner has to reconnect instagram from the responder settings page in the iris web app, unavailable means the feature isn't offered on the account yet. each message sent counts as a message against the plan. needs an API key and a connected instagram account; x402 and MPP identities cannot use it. at most 20 triggers per account, and a second trigger with the same keyword on the same post is refused. requires write scope.", {
|
|
123
|
+
keyword: z
|
|
124
|
+
.string()
|
|
125
|
+
.min(1)
|
|
126
|
+
.max(50)
|
|
127
|
+
.describe("matched as a whole word or phrase in the comment, case-insensitive (1-50 characters)"),
|
|
128
|
+
message: z
|
|
129
|
+
.string()
|
|
130
|
+
.min(1)
|
|
131
|
+
.max(900)
|
|
132
|
+
.refine((value) => messageByteLength(value) <= 700, {
|
|
133
|
+
message: "message must be at most 700 bytes (an emoji can cost up to 4 bytes)",
|
|
134
|
+
})
|
|
135
|
+
.describe("the DM sent to the commenter, before the automated notice (1-900 characters, and at most 700 bytes)"),
|
|
136
|
+
media_id: z
|
|
137
|
+
.union([z.string().regex(MEDIA_ID_PATTERN, "media_id must be 5 to 40 digits"), z.null()])
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("the numeric instagram media id to limit the trigger to one post (5-40 digits; get it from list_responder_posts); omit or send null to match every post"),
|
|
140
|
+
enabled: z.boolean().optional().describe("whether the trigger is active (default true)"),
|
|
141
|
+
}, async (args) => {
|
|
142
|
+
try {
|
|
143
|
+
const result = await client.post("/responder/comment-triggers", args);
|
|
144
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return formatMcpError(error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
server.tool("update_responder_comment_trigger", "update a comment trigger's keyword, message, media_id, or enabled state. give at least one field besides id. someone else's trigger, or an id that isn't a comment trigger on this account, is a 404. requires write scope.", {
|
|
151
|
+
id: z.string().uuid().describe("comment trigger ID"),
|
|
152
|
+
keyword: z
|
|
153
|
+
.string()
|
|
154
|
+
.min(1)
|
|
155
|
+
.max(50)
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("matched as a whole word or phrase in the comment, case-insensitive (1-50 characters)"),
|
|
158
|
+
message: z
|
|
159
|
+
.string()
|
|
160
|
+
.min(1)
|
|
161
|
+
.max(900)
|
|
162
|
+
.refine((value) => messageByteLength(value) <= 700, {
|
|
163
|
+
message: "message must be at most 700 bytes (an emoji can cost up to 4 bytes)",
|
|
164
|
+
})
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("the DM sent to the commenter, before the automated notice (1-900 characters, and at most 700 bytes)"),
|
|
167
|
+
media_id: z
|
|
168
|
+
.union([z.string().regex(MEDIA_ID_PATTERN, "media_id must be 5 to 40 digits"), z.null()])
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("the numeric instagram media id to limit the trigger to one post (5-40 digits; get it from list_responder_posts); null matches every post"),
|
|
171
|
+
enabled: z.boolean().optional().describe("whether the trigger is active"),
|
|
172
|
+
}, async ({ id, ...rest }) => {
|
|
173
|
+
const body = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
|
|
174
|
+
if (Object.keys(body).length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: JSON.stringify({
|
|
180
|
+
success: false,
|
|
181
|
+
error: {
|
|
182
|
+
code: "VALIDATION_ERROR",
|
|
183
|
+
message: "at least one field besides id is required",
|
|
184
|
+
},
|
|
185
|
+
}, null, 2),
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
isError: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const result = await client.patch(`/responder/comment-triggers/${id}`, body);
|
|
193
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return formatMcpError(error);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
server.tool("delete_responder_comment_trigger", "delete a comment trigger. requires write scope.", {
|
|
200
|
+
id: z.string().uuid().describe("comment trigger ID"),
|
|
201
|
+
}, async ({ id }) => {
|
|
202
|
+
try {
|
|
203
|
+
await client.delete(`/responder/comment-triggers/${id}`);
|
|
204
|
+
return { content: [{ type: "text", text: `comment trigger ${id} deleted` }] };
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return formatMcpError(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
server.tool("list_responder_posts", "list the account's recent instagram posts with their media ids, for limiting a comment trigger to one post. requires read scope.", {}, async () => {
|
|
211
|
+
try {
|
|
212
|
+
const result = await client.get("/responder/posts");
|
|
213
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return formatMcpError(error);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
package/dist/tools/webhooks.js
CHANGED
|
@@ -19,6 +19,8 @@ const WEBHOOK_EVENTS = [
|
|
|
19
19
|
"message.rejected",
|
|
20
20
|
"discovery.completed",
|
|
21
21
|
"discovery.failed",
|
|
22
|
+
"responder.reply_sent",
|
|
23
|
+
"responder.handoff",
|
|
22
24
|
];
|
|
23
25
|
export function registerWebhookTools(server, client) {
|
|
24
26
|
server.tool("list_webhooks", "list all configured webhooks. requires admin scope.", {
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ export interface Campaign {
|
|
|
33
33
|
follow_up_enabled: boolean;
|
|
34
34
|
follow_up_max_count: number;
|
|
35
35
|
follow_up_delay_days: number;
|
|
36
|
+
goal_description?: string | null;
|
|
37
|
+
analysis_status?: "none" | "pending" | "analyzing" | "completed" | "failed";
|
|
38
|
+
analysis_error?: string | null;
|
|
36
39
|
created_at: string;
|
|
37
40
|
updated_at: string;
|
|
38
41
|
}
|
|
@@ -43,14 +46,15 @@ export interface CampaignWithStats extends Campaign {
|
|
|
43
46
|
emails_opened?: number;
|
|
44
47
|
emails_replied?: number;
|
|
45
48
|
}
|
|
49
|
+
/** `GET /campaigns/{id}/usage` */
|
|
46
50
|
export interface CampaignUsage {
|
|
47
51
|
campaign_id: string;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
status: string;
|
|
53
|
+
total_contacts: number;
|
|
54
|
+
total_conversations: number;
|
|
55
|
+
delivery_status: Record<string, number>;
|
|
56
|
+
/** id of a contact search running for the campaign right now; null when none is */
|
|
57
|
+
discovery_job_id: string | null;
|
|
54
58
|
}
|
|
55
59
|
/** `POST /campaigns/{id}/contacts` */
|
|
56
60
|
export interface AddCampaignContactsResult {
|
|
@@ -323,3 +327,95 @@ export interface AudienceProfile {
|
|
|
323
327
|
created_at: string;
|
|
324
328
|
updated_at: string;
|
|
325
329
|
}
|
|
330
|
+
export interface ResponderConfig {
|
|
331
|
+
enabled: boolean;
|
|
332
|
+
auto_reply: boolean;
|
|
333
|
+
instructions: string | null;
|
|
334
|
+
booking_url: string | null;
|
|
335
|
+
qualifying_questions: string[];
|
|
336
|
+
disclosure: string | null;
|
|
337
|
+
max_replies_per_conversation: number;
|
|
338
|
+
paused: boolean;
|
|
339
|
+
pause_reason: string | null;
|
|
340
|
+
created_at: string;
|
|
341
|
+
updated_at: string;
|
|
342
|
+
}
|
|
343
|
+
/** `GET /responder` data shape. */
|
|
344
|
+
export interface ResponderStatus {
|
|
345
|
+
configured: boolean;
|
|
346
|
+
config: ResponderConfig | null;
|
|
347
|
+
status: {
|
|
348
|
+
instagram_connected: boolean;
|
|
349
|
+
replying: boolean;
|
|
350
|
+
/** when false, iris only sees replies sent through iris: pause a conversation before answering it from the Instagram app */
|
|
351
|
+
sees_instagram_app_replies: boolean;
|
|
352
|
+
paused: boolean;
|
|
353
|
+
pause_reason: string | null;
|
|
354
|
+
kill_switch: boolean;
|
|
355
|
+
/** whether comment triggers can send DMs right now: ready, needs the owner to reconnect instagram, or not offered on this account */
|
|
356
|
+
comment_triggers: "ready" | "needs_permission" | "unavailable";
|
|
357
|
+
};
|
|
358
|
+
last_30_days: {
|
|
359
|
+
threads: number;
|
|
360
|
+
replies_sent: number;
|
|
361
|
+
handoffs: number;
|
|
362
|
+
booking_links_sent: number;
|
|
363
|
+
comment_dms_sent: number;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* `GET /responder/comment-triggers`, `POST /responder/comment-triggers`,
|
|
368
|
+
* `PATCH /responder/comment-triggers/{id}`. A keyword that, when commented on
|
|
369
|
+
* an instagram post, sends the commenter one automatic DM.
|
|
370
|
+
*/
|
|
371
|
+
export interface ResponderCommentTrigger {
|
|
372
|
+
id: string;
|
|
373
|
+
keyword: string;
|
|
374
|
+
message: string;
|
|
375
|
+
media_id: string | null;
|
|
376
|
+
enabled: boolean;
|
|
377
|
+
sent_count: number;
|
|
378
|
+
last_sent_at: string | null;
|
|
379
|
+
created_at: string;
|
|
380
|
+
updated_at: string;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* `GET /responder/posts` list item. Up to 25 of the account's most recent
|
|
384
|
+
* instagram posts, newest first. `id` is the numeric instagram media id — pass
|
|
385
|
+
* it as `media_id` on a comment trigger to limit that trigger to one post.
|
|
386
|
+
*/
|
|
387
|
+
export interface ResponderPost {
|
|
388
|
+
id: string;
|
|
389
|
+
caption: string | null;
|
|
390
|
+
permalink: string | null;
|
|
391
|
+
media_type: string | null;
|
|
392
|
+
posted_at: string | null;
|
|
393
|
+
}
|
|
394
|
+
/** `GET /responder/activity` list item. */
|
|
395
|
+
export interface ResponderEvent {
|
|
396
|
+
id: string;
|
|
397
|
+
kind: "reply_sent" | "handoff" | "skipped" | "failed";
|
|
398
|
+
reason: string | null;
|
|
399
|
+
conversation_id: string | null;
|
|
400
|
+
contact_id: string | null;
|
|
401
|
+
message_id: string | null;
|
|
402
|
+
booking_link_sent: boolean;
|
|
403
|
+
created_at: string;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* `GET /conversations/{id}/responder` and `PATCH /conversations/{id}/responder`.
|
|
407
|
+
* `status` is null for a conversation the responder has never seen. `reason`
|
|
408
|
+
* is one of: human_replied, user_paused, contact_asked_for_person, needs_answer,
|
|
409
|
+
* sensitive, opted_out, reply_cap, message_limit, send_failed, outside_window, attachment,
|
|
410
|
+
* unsafe_reply.
|
|
411
|
+
*/
|
|
412
|
+
export interface ResponderThread {
|
|
413
|
+
conversation_id: string;
|
|
414
|
+
status: "active" | "paused" | "handed_off" | null;
|
|
415
|
+
reason: string | null;
|
|
416
|
+
origin: "dm" | "comment" | null;
|
|
417
|
+
replies_sent: number;
|
|
418
|
+
last_inbound_at: string | null;
|
|
419
|
+
last_reply_at: string | null;
|
|
420
|
+
booking_link_sent_at: string | null;
|
|
421
|
+
}
|