conduyt 1.57.0 → 1.58.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 +5 -0
- package/dist/index.js +271 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,11 @@ conduyt insights summary # run an AI insight query
|
|
|
40
40
|
conduyt insights saved_report --report "Lead Activity" --period last_7_days # run any saved report as a tool
|
|
41
41
|
conduyt insights dashboard --dashboard Floor --period today # run every tile of a board
|
|
42
42
|
conduyt insights report_catalog # what this key can run
|
|
43
|
+
conduyt sequences list --type sms --on # Email and SMS sequences (--archived, --summary)
|
|
44
|
+
conduyt sequences senders sms # who can send an SMS sequence (each member's line)
|
|
45
|
+
conduyt sequences stats <id> # sends, opens, clicks in total and per step
|
|
46
|
+
conduyt sequences people <id> --all-active # every live run; status and endedReason per run
|
|
47
|
+
conduyt sequences check <id> <contactId...> # who is eligible before enrolling
|
|
43
48
|
conduyt messages transports # what a workflow Send SMS step can speak through today (Twilio numbers, Project Blue lines, the WhatsApp sender)
|
|
44
49
|
conduyt whatsapp status # WhatsApp via your Twilio: connected? from which sender
|
|
45
50
|
conduyt whatsapp senders # the senders Twilio lists for the account (ONLINE = connectable)
|
package/dist/index.js
CHANGED
|
@@ -953,7 +953,7 @@ messages
|
|
|
953
953
|
})}`)));
|
|
954
954
|
messages
|
|
955
955
|
.command("send")
|
|
956
|
-
.description("Log/create a message (POST /messages). NOTE: outbound SMS is rejected here — use `messages send-sms` for real SMS delivery")
|
|
956
|
+
.description("Log/create a message (POST /messages). NOTE: outbound SMS is rejected here — use `messages send-sms` for real SMS delivery. An inbound SMS STOP or START with --from records that number's opt-out or opt-in: STOP blocks only the --from number; only when --from is the contact's PRIMARY number does STOP also end the contact's drips and the SMS sequences configured to stop on opt-out (autoUnenrollOnUnsubscribe) — a secondary number's STOP blocks that number alone, nothing else. The answer then carries smsConsent {keyword, fromNumber, applied, reason: number_not_on_contact | number_not_canonicalizable}")
|
|
957
957
|
.option("--contact <id>", "contact UUID (required)")
|
|
958
958
|
.option("--channel <channel>", "sms | email (required)")
|
|
959
959
|
.option("--direction <direction>", "inbound | outbound (required)")
|
|
@@ -1458,7 +1458,7 @@ automations
|
|
|
1458
1458
|
.action(run(async (client, opts) => client.post("/api/v1/automations/validate", bodyFromJsonOrFile(opts))));
|
|
1459
1459
|
automations
|
|
1460
1460
|
.command("import")
|
|
1461
|
-
.description("Create a workflow (with optional name resolution). Provide the body via --json or --file (needs name + triggerEvent + graph)")
|
|
1461
|
+
.description("Create a workflow (with optional name resolution). Provide the body via --json or --file (needs name + triggerEvent + graph). A sequence step is the action enroll_in_sequence with config {sequenceId, enrollAction: add (default) | remove}; add needs the sequence to allow automation enrollment")
|
|
1462
1462
|
.option("--json <json>", "full request body as JSON")
|
|
1463
1463
|
.option("--file <path>", "read the request body from a JSON file")
|
|
1464
1464
|
.option("--resolve-names", "set resolveNames=true so name fields in node configs are converted to ids")
|
|
@@ -2143,6 +2143,275 @@ webhooks
|
|
|
2143
2143
|
.option("--resend", "send the step again (duplicate risk: needs --acknowledge-duplicate-risk; refused 409 step_already_sent when the step already carries sent evidence: use --skip)")
|
|
2144
2144
|
.option("--acknowledge-duplicate-risk", "you accept the step may be delivered twice")
|
|
2145
2145
|
.action(run(async (client, id, opts) => client.post(`/api/v1/webhook-logs/${uuidArg(id, "id")}/cancel`, barrierBody(opts))));
|
|
2146
|
+
// ---------------------------------------------------------------------------
|
|
2147
|
+
// Sequences (one model for Email and SMS since 2026-09-30): steps send saved templates of the sequence's channel a
|
|
2148
|
+
// number of days apart inside a send window; a run stops on a reply, a booked meeting, an opt-out, a closed deal or a
|
|
2149
|
+
// removal. Provider drip timetables are the `drip-enrollments` group, a different thing.
|
|
2150
|
+
// ---------------------------------------------------------------------------
|
|
2151
|
+
const sequences = program.command("sequences").description("Email and SMS sequences built from saved templates: list, create, settings, On/Off, archive, enroll with a check first, who is in them and how each run ended");
|
|
2152
|
+
const SEQUENCE_SETTING_FLAGS = [
|
|
2153
|
+
["--sender <userId>", "the team member the messages are from (must be an active member): email from their on-domain address, SMS from their assigned line; pass none for the workspace default"],
|
|
2154
|
+
["--stop-on-reply <on|off>", "stop a contact's run when they reply to any message of the sequence (default on)"],
|
|
2155
|
+
["--stop-on-meeting <on|off>", "stop the run when a meeting is booked through any Conduyt calendar (default on)"],
|
|
2156
|
+
["--stop-on-opt-out <on|off>", "stop the run when the contact opts out: unsubscribe, or STOP for SMS (default on)"],
|
|
2157
|
+
["--stop-on-bounce <on|off>", "email only: stop the run on a hard bounce; a soft bounce waits and retries (default on)"],
|
|
2158
|
+
["--stop-on-deal-closed <on|off>", "stop a contact's run when any of their deals closes, won or lost, on any stage of a closed kind (default on)"],
|
|
2159
|
+
["--allow-automation-enroll <on|off>", "let the automation action enroll_in_sequence put contacts in (default on)"],
|
|
2160
|
+
["--allow-reenroll <on|off>", "let a contact go through it again once their earlier run ENDED (default off); a live or paused run is never restarted"],
|
|
2161
|
+
["--weekdays-only <on|off>", "send Monday to Friday only, in the contact's time zone (default on)"],
|
|
2162
|
+
["--send-from <hour>", "first hour of the send window, 0-23 local (default 9)"],
|
|
2163
|
+
["--send-until <hour>", "end of the send window, 1-24 local, exclusive (default 17); a step due outside the window waits for the next opening"],
|
|
2164
|
+
];
|
|
2165
|
+
function sequenceSettingsBody(opts) {
|
|
2166
|
+
const body = {};
|
|
2167
|
+
if (opts.sender !== undefined)
|
|
2168
|
+
body.senderUserId = opts.sender === "none" ? null : uuidArg(opts.sender, "--sender");
|
|
2169
|
+
if (opts.stopOnReply !== undefined)
|
|
2170
|
+
body.autoUnenrollOnReply = onOffArg(opts.stopOnReply, "--stop-on-reply");
|
|
2171
|
+
if (opts.stopOnMeeting !== undefined)
|
|
2172
|
+
body.autoUnenrollOnMeeting = onOffArg(opts.stopOnMeeting, "--stop-on-meeting");
|
|
2173
|
+
if (opts.stopOnOptOut !== undefined)
|
|
2174
|
+
body.autoUnenrollOnUnsubscribe = onOffArg(opts.stopOnOptOut, "--stop-on-opt-out");
|
|
2175
|
+
if (opts.stopOnBounce !== undefined)
|
|
2176
|
+
body.autoUnenrollOnBounce = onOffArg(opts.stopOnBounce, "--stop-on-bounce");
|
|
2177
|
+
if (opts.stopOnDealClosed !== undefined)
|
|
2178
|
+
body.autoUnenrollOnDealClosed = onOffArg(opts.stopOnDealClosed, "--stop-on-deal-closed");
|
|
2179
|
+
if (opts.allowAutomationEnroll !== undefined)
|
|
2180
|
+
body.allowAutomationEnroll = onOffArg(opts.allowAutomationEnroll, "--allow-automation-enroll");
|
|
2181
|
+
if (opts.allowReenroll !== undefined)
|
|
2182
|
+
body.allowReenroll = onOffArg(opts.allowReenroll, "--allow-reenroll");
|
|
2183
|
+
if (opts.weekdaysOnly !== undefined)
|
|
2184
|
+
body.weekdaysOnly = onOffArg(opts.weekdaysOnly, "--weekdays-only");
|
|
2185
|
+
for (const [flag, key, lo, hi] of [["sendFrom", "sendStartHour", 0, 23], ["sendUntil", "sendEndHour", 1, 24]]) {
|
|
2186
|
+
const raw = opts[flag];
|
|
2187
|
+
if (raw === undefined)
|
|
2188
|
+
continue;
|
|
2189
|
+
const n = Number(raw);
|
|
2190
|
+
if (!Number.isInteger(n) || n < lo || n > hi)
|
|
2191
|
+
throw new Error(`--${flag === "sendFrom" ? "send-from" : "send-until"} must be a whole hour, ${lo} to ${hi}.`);
|
|
2192
|
+
body[key] = n;
|
|
2193
|
+
}
|
|
2194
|
+
return body;
|
|
2195
|
+
}
|
|
2196
|
+
/** --steps: JSON array of {templateId, waitDays}, or "tpl-id:days,tpl-id:days" (the first step's wait is 0) */
|
|
2197
|
+
function sequenceStepsArg(raw) {
|
|
2198
|
+
const t = raw.trim();
|
|
2199
|
+
if (t.startsWith("[")) {
|
|
2200
|
+
let v;
|
|
2201
|
+
try {
|
|
2202
|
+
v = JSON.parse(t);
|
|
2203
|
+
}
|
|
2204
|
+
catch {
|
|
2205
|
+
throw new Error("--steps must be a JSON array of {templateId, waitDays} or a list like id:0,id:3.");
|
|
2206
|
+
}
|
|
2207
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
2208
|
+
throw new Error("--steps needs at least one step.");
|
|
2209
|
+
return v.map((row, i) => {
|
|
2210
|
+
const r = (row ?? {});
|
|
2211
|
+
if (typeof r.templateId !== "string")
|
|
2212
|
+
throw new Error(`--steps[${i}] needs a templateId.`);
|
|
2213
|
+
const wait = r.waitDays === undefined ? (i === 0 ? 0 : 1) : Number(r.waitDays);
|
|
2214
|
+
if (!Number.isInteger(wait) || wait < 0)
|
|
2215
|
+
throw new Error(`--steps[${i}].waitDays must be a whole number of days.`);
|
|
2216
|
+
return { templateId: r.templateId, waitDays: i === 0 ? 0 : wait };
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
const parts = t.split(",").map((p) => p.trim()).filter(Boolean);
|
|
2220
|
+
if (parts.length === 0)
|
|
2221
|
+
throw new Error("--steps needs at least one step.");
|
|
2222
|
+
return parts.map((p, i) => {
|
|
2223
|
+
const [id, days] = p.split(":");
|
|
2224
|
+
const wait = days === undefined ? (i === 0 ? 0 : 1) : Number(days);
|
|
2225
|
+
if (!Number.isInteger(wait) || wait < 0)
|
|
2226
|
+
throw new Error(`--steps step ${i + 1}: the wait must be a whole number of days (templateId:days).`);
|
|
2227
|
+
return { templateId: uuidArg(id, "--steps"), waitDays: i === 0 ? 0 : wait };
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
function withSequenceSettingFlags(cmd) {
|
|
2231
|
+
let c = cmd;
|
|
2232
|
+
for (const [flag, desc] of SEQUENCE_SETTING_FLAGS)
|
|
2233
|
+
c = c.option(flag, desc);
|
|
2234
|
+
return c;
|
|
2235
|
+
}
|
|
2236
|
+
sequences
|
|
2237
|
+
.command("list")
|
|
2238
|
+
.description("List sequences (GET /emails/sequences), newest first: type (email | sms), isActive (On/Off), steps, senderUserId and sender, the stop rules, allowAutomationEnroll, allowReenroll, weekdaysOnly, sendStartHour, sendEndHour, archivedAt, people in it now. Archived ones are left out unless --archived. An API key sees only the channel(s) it is scoped to")
|
|
2239
|
+
.option("--type <email|sms>", "only this channel")
|
|
2240
|
+
.option("--on", "only sequences that are On")
|
|
2241
|
+
.option("--off", "only sequences that are Off")
|
|
2242
|
+
.option("--archived", "archived sequences only")
|
|
2243
|
+
.option("--summary", "a light row per sequence (view=summary): id, name, type, isActive, dates, enrolledCount, description cut to 300 characters, stepCount")
|
|
2244
|
+
.option("--page <n>", "page number")
|
|
2245
|
+
.option("--limit <n>", "results per page (per_page, max 200)")
|
|
2246
|
+
.action(run(async (client, opts) => {
|
|
2247
|
+
const params = {};
|
|
2248
|
+
if (opts.type !== undefined) {
|
|
2249
|
+
if (opts.type !== "email" && opts.type !== "sms")
|
|
2250
|
+
throw new Error("--type must be email or sms.");
|
|
2251
|
+
params.type = opts.type;
|
|
2252
|
+
}
|
|
2253
|
+
if (opts.on && opts.off)
|
|
2254
|
+
throw new Error("--on and --off exclude each other.");
|
|
2255
|
+
if (opts.on)
|
|
2256
|
+
params.is_active = "true";
|
|
2257
|
+
if (opts.off)
|
|
2258
|
+
params.is_active = "false";
|
|
2259
|
+
if (opts.archived)
|
|
2260
|
+
params.archived = "true";
|
|
2261
|
+
if (opts.summary)
|
|
2262
|
+
params.view = "summary";
|
|
2263
|
+
if (opts.page !== undefined)
|
|
2264
|
+
params.page = opts.page;
|
|
2265
|
+
if (opts.limit !== undefined)
|
|
2266
|
+
params.per_page = opts.limit;
|
|
2267
|
+
return client.get(`/api/v1/emails/sequences${buildQuery(params)}`);
|
|
2268
|
+
}));
|
|
2269
|
+
sequences
|
|
2270
|
+
.command("get <id>")
|
|
2271
|
+
.description("One sequence with its steps (template per step, wait in days), sender, send window, stop rules, enrollment switches, archivedAt, people in it now and enrollmentStats (people per run status)")
|
|
2272
|
+
.action(run(async (client, id) => client.get(`/api/v1/emails/sequences/${uuidArg(id, "id")}`)));
|
|
2273
|
+
withSequenceSettingFlags(sequences
|
|
2274
|
+
.command("create <name>")
|
|
2275
|
+
.description("Create an Email or SMS sequence from saved templates (POST /emails/sequences). Created Off unless --on. The type cannot change afterwards")
|
|
2276
|
+
.option("--type <email|sms>", "the channel (default email)")
|
|
2277
|
+
.option("--steps <steps>", "the steps in order: templateId:days,templateId:days (the first step's wait is 0) or a JSON array of {templateId, waitDays}")
|
|
2278
|
+
.option("--description <text>", "description")
|
|
2279
|
+
.option("--on", "turn it On right away")
|
|
2280
|
+
.option("--json <json>", "full JSON body, merged over the flags")).action(run(async (client, name, opts) => {
|
|
2281
|
+
const body = { name, ...sequenceSettingsBody(opts) };
|
|
2282
|
+
if (opts.type !== undefined) {
|
|
2283
|
+
if (opts.type !== "email" && opts.type !== "sms")
|
|
2284
|
+
throw new Error("--type must be email or sms.");
|
|
2285
|
+
body.type = opts.type;
|
|
2286
|
+
}
|
|
2287
|
+
if (opts.steps !== undefined)
|
|
2288
|
+
body.steps = sequenceStepsArg(opts.steps);
|
|
2289
|
+
if (opts.description !== undefined)
|
|
2290
|
+
body.description = opts.description;
|
|
2291
|
+
if (opts.on)
|
|
2292
|
+
body.isActive = true;
|
|
2293
|
+
if (opts.json !== undefined)
|
|
2294
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
2295
|
+
if (!Array.isArray(body.steps) || body.steps.length === 0)
|
|
2296
|
+
throw new Error("--steps is required: at least one template.");
|
|
2297
|
+
return client.post("/api/v1/emails/sequences", body);
|
|
2298
|
+
}));
|
|
2299
|
+
withSequenceSettingFlags(sequences
|
|
2300
|
+
.command("update <id>")
|
|
2301
|
+
.description("Change a sequence (PATCH /emails/sequences/:id): name, steps, sender, send window, stop rules, enrollment switches. The type cannot change. While people are in it, the steps they already received cannot be changed, removed or reordered (409 sequence_steps_in_use); later steps change and new ones append")
|
|
2302
|
+
.option("--name <name>", "new name")
|
|
2303
|
+
.option("--description <text>", "description")
|
|
2304
|
+
.option("--steps <steps>", "the whole step list, in order (replaces the current steps): templateId:days,... or a JSON array")
|
|
2305
|
+
.option("--json <json>", "full JSON body, merged over the flags")).action(run(async (client, id, opts) => {
|
|
2306
|
+
const body = sequenceSettingsBody(opts);
|
|
2307
|
+
if (opts.name !== undefined)
|
|
2308
|
+
body.name = opts.name;
|
|
2309
|
+
if (opts.description !== undefined)
|
|
2310
|
+
body.description = opts.description;
|
|
2311
|
+
if (opts.steps !== undefined)
|
|
2312
|
+
body.steps = sequenceStepsArg(opts.steps);
|
|
2313
|
+
if (opts.json !== undefined)
|
|
2314
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
2315
|
+
if (Object.keys(body).length === 0)
|
|
2316
|
+
throw new Error("Nothing to change: pass at least one flag.");
|
|
2317
|
+
return client.patch(`/api/v1/emails/sequences/${uuidArg(id, "id")}`, body);
|
|
2318
|
+
}));
|
|
2319
|
+
sequences
|
|
2320
|
+
.command("on <id>")
|
|
2321
|
+
.description("Turn a sequence On: its runs continue from where they are")
|
|
2322
|
+
.action(run(async (client, id) => client.patch(`/api/v1/emails/sequences/${uuidArg(id, "id")}`, { isActive: true })));
|
|
2323
|
+
sequences
|
|
2324
|
+
.command("off <id>")
|
|
2325
|
+
.description("Turn a sequence Off: every run stays where it is, nothing sends, until it is turned On again")
|
|
2326
|
+
.action(run(async (client, id) => client.patch(`/api/v1/emails/sequences/${uuidArg(id, "id")}`, { isActive: false })));
|
|
2327
|
+
sequences
|
|
2328
|
+
.command("archive <id>")
|
|
2329
|
+
.description("Archive a sequence: Off and hidden from lists (see `sequences list --archived`)")
|
|
2330
|
+
.option("--restore", "bring an archived sequence back (stays Off until turned On)")
|
|
2331
|
+
.action(run(async (client, id, opts) => client.patch(`/api/v1/emails/sequences/${uuidArg(id, "id")}`, { archived: !opts.restore })));
|
|
2332
|
+
sequences
|
|
2333
|
+
.command("check <id> <contactIds...>")
|
|
2334
|
+
.description("Check contacts BEFORE enrolling them (POST /emails/sequences/:id/preflight, up to 500): total, eligible, skipped, errors (blockers: Off, archived, a missing template, an unverified domain; any blocker makes nobody eligible), warnings, contacts [{contactId, eligible, reasons}]. Nothing is written")
|
|
2335
|
+
.action(run(async (client, id, contactIds) => client.post(`/api/v1/emails/sequences/${uuidArg(id, "id")}/preflight`, { contactIds: sequenceContactIds(contactIds) })));
|
|
2336
|
+
sequences
|
|
2337
|
+
.command("senders <channel>")
|
|
2338
|
+
.description("Who can be a sequence's sender (GET /emails/sequences/senders), for email or sms: every active member who accepted their invite; sms lists each one's assigned line (an SMS sequence needs one), email each one's address")
|
|
2339
|
+
.action(run(async (client, channel) => {
|
|
2340
|
+
if (channel !== "email" && channel !== "sms")
|
|
2341
|
+
throw new Error("channel must be email or sms.");
|
|
2342
|
+
return client.get(`/api/v1/emails/sequences/senders?channel=${channel}`);
|
|
2343
|
+
}));
|
|
2344
|
+
sequences
|
|
2345
|
+
.command("pick-list <id>")
|
|
2346
|
+
.description("Who of a saved list goes into the sequence next (POST /emails/sequences/:id/pick-from-list): the server walks the list in a stable order, leaves out whoever is already in, checks the rest, and returns up to --limit eligible contacts plus a cursor. Enroll the ids, then call again with --cursor until exhausted. Nothing is written")
|
|
2347
|
+
.option("--smart-view <id>", "the Smart View to pick from (one of --smart-view / --smart-list)")
|
|
2348
|
+
.option("--smart-list <id>", "the Smart List to pick from")
|
|
2349
|
+
.option("--limit <n>", "eligible contacts to return, 1-500 (default 500)")
|
|
2350
|
+
.option("--cursor <cursor>", "where to continue, from the previous call")
|
|
2351
|
+
.action(run(async (client, id, opts) => {
|
|
2352
|
+
if (!opts.smartView && !opts.smartList)
|
|
2353
|
+
throw new Error("Pass --smart-view or --smart-list.");
|
|
2354
|
+
if (opts.smartView && opts.smartList)
|
|
2355
|
+
throw new Error("Pass one of --smart-view or --smart-list.");
|
|
2356
|
+
const body = {};
|
|
2357
|
+
if (opts.smartView)
|
|
2358
|
+
body.smartViewId = uuidArg(opts.smartView, "--smart-view");
|
|
2359
|
+
if (opts.smartList)
|
|
2360
|
+
body.smartListId = uuidArg(opts.smartList, "--smart-list");
|
|
2361
|
+
if (opts.limit !== undefined) {
|
|
2362
|
+
const n = Number(opts.limit);
|
|
2363
|
+
if (!Number.isInteger(n) || n < 1 || n > 500)
|
|
2364
|
+
throw new Error("--limit must be 1 to 500.");
|
|
2365
|
+
body.limit = n;
|
|
2366
|
+
}
|
|
2367
|
+
if (opts.cursor !== undefined)
|
|
2368
|
+
body.cursor = opts.cursor;
|
|
2369
|
+
return client.post(`/api/v1/emails/sequences/${uuidArg(id, "id")}/pick-from-list`, body);
|
|
2370
|
+
}));
|
|
2371
|
+
sequences
|
|
2372
|
+
.command("enroll <id> <contactIds...>")
|
|
2373
|
+
.description("Enroll contacts (POST /emails/sequences/:id/enroll, up to 500; the sequence must be On and not archived): skips anyone with no address for the channel, opted out, or already in it; with re-enrollment allowed, an ENDED run starts over (a live or paused one never restarts). Returns enrolled, skipped, duplicates, total, skippedDetails")
|
|
2374
|
+
.action(run(async (client, id, contactIds) => client.post(`/api/v1/emails/sequences/${uuidArg(id, "id")}/enroll`, { contactIds: sequenceContactIds(contactIds) })));
|
|
2375
|
+
sequences
|
|
2376
|
+
.command("unenroll <id> <contactId>")
|
|
2377
|
+
.description("Remove a contact from a sequence (POST /emails/sequences/:id/unenroll): their live, paused or failed run ends (status cancelled, endedReason removed). Returns contactId, unenrolled, currentStatus")
|
|
2378
|
+
.action(run(async (client, id, contactId) => client.post(`/api/v1/emails/sequences/${uuidArg(id, "id")}/unenroll`, { contactId: uuidArg(contactId, "contactId") })));
|
|
2379
|
+
const SEQUENCE_RUN_STATUSES = ["active", "paused", "completed", "failed", "cancelled", "unsubscribed"];
|
|
2380
|
+
sequences
|
|
2381
|
+
.command("people <id>")
|
|
2382
|
+
.description("Who is in a sequence (GET /emails/sequences/:id/enrollments), newest run first: contact, currentStep, nextSendAt, engagement, status (active | paused | completed | failed | cancelled | unsubscribed) and endedReason (completed | replied | meeting | opted_out | bounced | deal_closed | removed; null while live). A paused run with no endedReason is live: it resumes when the sequence is turned On")
|
|
2383
|
+
.option("--status <status>", "active | paused | completed | failed | cancelled | unsubscribed")
|
|
2384
|
+
.option("--all-active", "every live run in one answer (all_active=true): no paging, no engagement; refused past 5,000")
|
|
2385
|
+
.option("--page <n>", "page number")
|
|
2386
|
+
.option("--limit <n>", "results per page (per_page, max 200)")
|
|
2387
|
+
.action(run(async (client, id, opts) => {
|
|
2388
|
+
const params = {};
|
|
2389
|
+
if (opts.status !== undefined) {
|
|
2390
|
+
if (!SEQUENCE_RUN_STATUSES.includes(opts.status))
|
|
2391
|
+
throw new Error(`--status must be one of ${SEQUENCE_RUN_STATUSES.join(", ")}.`);
|
|
2392
|
+
params.status = opts.status;
|
|
2393
|
+
}
|
|
2394
|
+
if (opts.allActive)
|
|
2395
|
+
params.all_active = "true";
|
|
2396
|
+
if (opts.page !== undefined)
|
|
2397
|
+
params.page = opts.page;
|
|
2398
|
+
if (opts.limit !== undefined)
|
|
2399
|
+
params.per_page = opts.limit;
|
|
2400
|
+
return client.get(`/api/v1/emails/sequences/${uuidArg(id, "id")}/enrollments${buildQuery(params)}`);
|
|
2401
|
+
}));
|
|
2402
|
+
sequences
|
|
2403
|
+
.command("stats <id>")
|
|
2404
|
+
.description("Performance of a sequence (GET /emails/sequences/:id/stats): totals {enrolled (runs started, all time), contacts (people in it now), sent, opened, clicked, loggedSends, openRate, clickRate}, perStep and removedSteps")
|
|
2405
|
+
.action(run(async (client, id) => client.get(`/api/v1/emails/sequences/${uuidArg(id, "id")}/stats`)));
|
|
2406
|
+
/** contact ids as given, or a comma-separated list in one argument; 1 to 500 UUIDs */
|
|
2407
|
+
function sequenceContactIds(args) {
|
|
2408
|
+
const ids = args.flatMap((a) => a.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
2409
|
+
if (ids.length === 0)
|
|
2410
|
+
throw new Error("Pass at least one contact id.");
|
|
2411
|
+
if (ids.length > 500)
|
|
2412
|
+
throw new Error("At most 500 contacts per call.");
|
|
2413
|
+
return ids.map((id) => uuidArg(id, "contactId"));
|
|
2414
|
+
}
|
|
2146
2415
|
const dripEnrollments = program.command("drip-enrollments").description("Drip enrollment recovery");
|
|
2147
2416
|
dripEnrollments
|
|
2148
2417
|
.command("cancel-barrier <id>")
|
package/package.json
CHANGED