conduyt 1.43.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +239 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -960,6 +960,109 @@ fromk
|
|
|
960
960
|
throw new Error("Pass --secret or set FROMK_WEBHOOK_SECRET (or --clear).");
|
|
961
961
|
return client.put("/api/v1/settings/fromk/webhook", { secret });
|
|
962
962
|
}));
|
|
963
|
+
// ---- Zoom (Conduyt #72, 2026-09-15): one Zoom meeting per booking ----
|
|
964
|
+
const zoom = program.command("zoom").description("Zoom: the account's Server-to-Server OAuth app; booking pages set to Zoom create one meeting per booking, delete it on cancel, move it on reschedule (settings scope)");
|
|
965
|
+
zoom
|
|
966
|
+
.command("status")
|
|
967
|
+
.description("Connection status (GET /settings/zoom): connected | not_configured, the account id and client id masked; the client secret is never returned")
|
|
968
|
+
.action(run(async (client) => client.get("/api/v1/settings/zoom")));
|
|
969
|
+
zoom
|
|
970
|
+
.command("connect")
|
|
971
|
+
.description("Connect or replace the Server-to-Server app (scopes meeting:write/read/update/delete:meeting:admin + user:read:user:admin) — verified with Zoom before it is stored; answers the app's own user = the default meeting host")
|
|
972
|
+
.option("--account-id <id>", "Zoom account id; omit to read ZOOM_ACCOUNT_ID from the environment")
|
|
973
|
+
.option("--client-id <id>", "Zoom client id; omit to read ZOOM_CLIENT_ID")
|
|
974
|
+
.option("--client-secret <secret>", "Zoom client secret; omit to read ZOOM_CLIENT_SECRET and keep it out of shell history")
|
|
975
|
+
.option("--force", "replace the app although upcoming bookings still hold meetings the current app created (409 zoom_meetings_active otherwise; those meetings stay on the old Zoom account)")
|
|
976
|
+
.action(run(async (client, opts) => {
|
|
977
|
+
const accountId = (opts.accountId ?? process.env.ZOOM_ACCOUNT_ID ?? "").trim();
|
|
978
|
+
const clientId = (opts.clientId ?? process.env.ZOOM_CLIENT_ID ?? "").trim();
|
|
979
|
+
const clientSecret = (opts.clientSecret ?? process.env.ZOOM_CLIENT_SECRET ?? "").trim();
|
|
980
|
+
if (!accountId || !clientId || !clientSecret)
|
|
981
|
+
throw new Error("Pass --account-id, --client-id and --client-secret (or set ZOOM_ACCOUNT_ID / ZOOM_CLIENT_ID / ZOOM_CLIENT_SECRET).");
|
|
982
|
+
return client.put("/api/v1/settings/zoom", { accountId, clientId, clientSecret, ...(opts.force ? { force: true } : {}) });
|
|
983
|
+
}));
|
|
984
|
+
zoom
|
|
985
|
+
.command("disconnect")
|
|
986
|
+
.description("Forget the app; booking pages set to Zoom fall back to their written location. Refused (409 zoom_meetings_active) while upcoming bookings hold meetings this app created unless --force")
|
|
987
|
+
.option("--force", "disconnect anyway; those meetings stay on the Zoom account and cancellations record zoom_orphaned")
|
|
988
|
+
.action(run(async (client, opts) => client.del(`/api/v1/settings/zoom${opts.force ? "?force=1" : ""}`)));
|
|
989
|
+
zoom
|
|
990
|
+
.command("test")
|
|
991
|
+
.description("Prove the stored app works (a token is issued, the app's user answers); --host also proves that user belongs to the Zoom account (the value a booking page's --video-host must be). Read-only on Zoom")
|
|
992
|
+
.option("--host <email>", "a Zoom user of the connected account to verify as a meeting host")
|
|
993
|
+
.action(run(async (client, opts) => client.post("/api/v1/settings/zoom/test", opts.host ? { hostEmail: opts.host } : {})));
|
|
994
|
+
// ---- Booking pages (Conduyt #72, 2026-09-15): the public scheduling pages and their video option ----
|
|
995
|
+
const bookingPages = program.command("booking-pages").description("Booking pages: the public scheduling pages (slug, host, duration, availability, embed origins) and their video option (calendar scope)");
|
|
996
|
+
bookingPages
|
|
997
|
+
.command("list")
|
|
998
|
+
.description("List the account's booking pages (GET /booking-pages)")
|
|
999
|
+
.option("--active", "only active pages")
|
|
1000
|
+
.action(run(async (client, opts) => client.get(`/api/v1/booking-pages${opts.active ? "?is_active=true" : ""}`)));
|
|
1001
|
+
bookingPages
|
|
1002
|
+
.command("get <id>")
|
|
1003
|
+
.description("One booking page with every setting (GET /booking-pages/:id)")
|
|
1004
|
+
.action(run(async (client, id) => client.get(`/api/v1/booking-pages/${id}`)));
|
|
1005
|
+
const bookingPageBody = (opts) => {
|
|
1006
|
+
const body = {};
|
|
1007
|
+
const set = (key, value) => { if (value !== undefined)
|
|
1008
|
+
body[key] = value; };
|
|
1009
|
+
set("title", opts.title);
|
|
1010
|
+
set("slug", opts.slug);
|
|
1011
|
+
set("description", opts.description);
|
|
1012
|
+
set("userId", opts.host);
|
|
1013
|
+
set("location", opts.location);
|
|
1014
|
+
if (opts.videoProvider !== undefined)
|
|
1015
|
+
set("videoProvider", opts.videoProvider === "none" ? null : opts.videoProvider);
|
|
1016
|
+
if (opts.videoHost !== undefined)
|
|
1017
|
+
set("videoHostEmail", opts.videoHost === "none" ? null : opts.videoHost);
|
|
1018
|
+
if (opts.duration !== undefined)
|
|
1019
|
+
set("durationMinutes", Number(opts.duration));
|
|
1020
|
+
if (opts.minimumNotice !== undefined)
|
|
1021
|
+
set("minimumNotice", Number(opts.minimumNotice));
|
|
1022
|
+
if (opts.maxAdvanceDays !== undefined)
|
|
1023
|
+
set("maxAdvanceDays", Number(opts.maxAdvanceDays));
|
|
1024
|
+
set("leadSource", opts.leadSource);
|
|
1025
|
+
set("brandName", opts.brandName);
|
|
1026
|
+
if (typeof opts.embedOrigins === "string")
|
|
1027
|
+
set("embedAllowedOrigins", opts.embedOrigins.split(",").map((o) => o.trim()).filter(Boolean));
|
|
1028
|
+
if (typeof opts.availability === "string")
|
|
1029
|
+
set("availabilityRules", JSON.parse(opts.availability));
|
|
1030
|
+
if (opts.active !== undefined)
|
|
1031
|
+
set("isActive", opts.active);
|
|
1032
|
+
return body;
|
|
1033
|
+
};
|
|
1034
|
+
const bookingPageOptions = (cmd) => cmd
|
|
1035
|
+
.option("--title <title>", "page title")
|
|
1036
|
+
.option("--slug <slug>", "public slug (conduyt.app/book/<slug>)")
|
|
1037
|
+
.option("--description <text>", "description shown on the page")
|
|
1038
|
+
.option("--host <userId>", "host user UUID (the page owner)")
|
|
1039
|
+
.option("--duration <minutes>", "slot length in minutes")
|
|
1040
|
+
.option("--minimum-notice <minutes>", "earliest booking, minutes from now")
|
|
1041
|
+
.option("--max-advance-days <days>", "how far ahead visitors may book")
|
|
1042
|
+
.option("--location <text>", "written location (Zoom link, phone, address); with --video-provider zoom it is the fallback when Zoom is down")
|
|
1043
|
+
.option("--video-provider <zoom|none>", "zoom = a new Zoom meeting for every booking (needs `conduyt zoom connect`); none = the written location only")
|
|
1044
|
+
.option("--video-host <email|none>", "the Zoom user the meeting is created under (a user of the connected Zoom account); none = the app's own user")
|
|
1045
|
+
.option("--lead-source <source>", "lead source stamped on contacts the page creates")
|
|
1046
|
+
.option("--brand-name <name>", "white-label name shown on the page and in Zoom meeting names")
|
|
1047
|
+
.option("--embed-origins <origins>", "comma-separated origins allowed to embed/call the page (empty = any)")
|
|
1048
|
+
.option("--availability <json>", "AvailabilityWindow[] JSON override, e.g. '[{\"dayOfWeek\":1,\"startTime\":\"09:00\",\"endTime\":\"11:00\"}]'")
|
|
1049
|
+
.option("--active", "page accepts bookings")
|
|
1050
|
+
.option("--no-active", "page stops accepting bookings");
|
|
1051
|
+
bookingPageOptions(bookingPages.command("create").description("Create a booking page (POST /booking-pages): --title, --slug, --host and --duration at least"))
|
|
1052
|
+
.action(run(async (client, opts) => {
|
|
1053
|
+
const body = bookingPageBody(opts);
|
|
1054
|
+
for (const k of ["title", "slug", "userId", "durationMinutes"])
|
|
1055
|
+
if (body[k] === undefined)
|
|
1056
|
+
throw new Error(`--${k === "userId" ? "host" : k === "durationMinutes" ? "duration" : k} is required`);
|
|
1057
|
+
return client.post("/api/v1/booking-pages", body);
|
|
1058
|
+
}));
|
|
1059
|
+
bookingPageOptions(bookingPages.command("update <id>").description("Update a booking page (PATCH /booking-pages/:id): only the options given change"))
|
|
1060
|
+
.action(run(async (client, id, opts) => {
|
|
1061
|
+
const body = bookingPageBody(opts);
|
|
1062
|
+
if (Object.keys(body).length === 0)
|
|
1063
|
+
throw new Error("Nothing to update: pass at least one option.");
|
|
1064
|
+
return client.patch(`/api/v1/booking-pages/${id}`, body);
|
|
1065
|
+
}));
|
|
963
1066
|
// ---- WhatsApp via Twilio (2026-09-11) ----
|
|
964
1067
|
const whatsapp = program.command("whatsapp").description("WhatsApp via your own Twilio account: a Meta-approved sender, approved templates for outreach, free-form replies within a contact's 24-hour window");
|
|
965
1068
|
whatsapp
|
|
@@ -1305,6 +1408,142 @@ automations
|
|
|
1305
1408
|
return client.get(path);
|
|
1306
1409
|
}));
|
|
1307
1410
|
// ---- companies ----
|
|
1411
|
+
// ─── Run Batch (#80) ───────────────────────────────────────────────────────
|
|
1412
|
+
const batch = program.command("batch").description("Run Batch: one logged task on many contacts (enroll, delete, add-tag, remove-tag, assign, distribute, set-source, set-status), with progress and cancel; admins and owners only");
|
|
1413
|
+
batch
|
|
1414
|
+
.command("create")
|
|
1415
|
+
.description("Queue a Run Batch. Pick the contacts with --ids, or freeze EVERY contact a list filter matches with the same flags as `contacts list` plus --expected (the total the list showed; the server refuses with 409 when the set moved). One task per run: enroll (--workflow), delete, add-tag/remove-tag (--tag-id), assign (--user), distribute (--users, round robin), set-source (--source), set-status (--status). No undo; `batch cancel` stops a run.")
|
|
1416
|
+
.requiredOption("--name <name>", "a name for the run")
|
|
1417
|
+
.requiredOption("--task <task>", "enroll | delete | add-tag | remove-tag | assign | distribute | set-source | set-status")
|
|
1418
|
+
.option("--workflow <automationId>", "enroll: the published workflow id")
|
|
1419
|
+
.option("--tag-id <tagId>", "add-tag / remove-tag: the tag id")
|
|
1420
|
+
.option("--user <userId>", "assign: one rep")
|
|
1421
|
+
.option("--users <userIds>", "distribute: comma-separated rep ids (2–50), rotated in order")
|
|
1422
|
+
.option("--source <source>", "set-source: the new lead source (overwrites)")
|
|
1423
|
+
.option("--status <status>", "set-status: one of the account's lead statuses")
|
|
1424
|
+
.option("--comment <text>", "why this run (stored with it)")
|
|
1425
|
+
.option("--ids <contactIds>", "comma-separated contact ids (instead of a filter)")
|
|
1426
|
+
.option("--search <text>", "filter: free-text search")
|
|
1427
|
+
.option("--tag <name>", "filter: one tag")
|
|
1428
|
+
.option("--tags <names>", "filter: comma-separated tags")
|
|
1429
|
+
.option("--filter-source <source>", "filter: lead source")
|
|
1430
|
+
.option("--assigned-to <userId>", "filter: assignee user id")
|
|
1431
|
+
.option("--smart-view <id>", "filter: Smart View id")
|
|
1432
|
+
.option("--smart-list <id>", "filter: Smart List id")
|
|
1433
|
+
.option("--import-id <id>", "filter: import job id")
|
|
1434
|
+
.option("--master-status <statuses>", "filter: comma-separated lead statuses to include")
|
|
1435
|
+
.option("--filter <json>", "filter: a JSON object of list query parameters (merged over the filter flags)")
|
|
1436
|
+
.option("--expected <n>", "filter: the total the list showed for this filter (required with a filter)")
|
|
1437
|
+
.action(run(async (client, opts) => {
|
|
1438
|
+
const TASKS = { enroll: "enroll", delete: "delete", "add-tag": "add_tag", "remove-tag": "remove_tag", assign: "assign", distribute: "assign", "set-source": "set_source", "set-status": "set_status" };
|
|
1439
|
+
const task = String(opts.task ?? "");
|
|
1440
|
+
const operation = TASKS[task];
|
|
1441
|
+
if (!operation)
|
|
1442
|
+
throw new Error("--task must be one of: enroll, delete, add-tag, remove-tag, assign, distribute, set-source, set-status");
|
|
1443
|
+
const params = {};
|
|
1444
|
+
if (task === "add-tag" || task === "remove-tag") {
|
|
1445
|
+
if (!opts.tagId)
|
|
1446
|
+
throw new Error(`--tag-id is required for ${task}`);
|
|
1447
|
+
params.tagId = opts.tagId;
|
|
1448
|
+
}
|
|
1449
|
+
else if (task === "assign") {
|
|
1450
|
+
if (!opts.user)
|
|
1451
|
+
throw new Error("--user is required for assign (use distribute with --users for a pool)");
|
|
1452
|
+
params.userId = opts.user;
|
|
1453
|
+
}
|
|
1454
|
+
else if (task === "distribute") {
|
|
1455
|
+
const users = csvArg(opts.users ?? "", "--users");
|
|
1456
|
+
if (users.length < 2)
|
|
1457
|
+
throw new Error("--users needs at least 2 rep ids for distribute");
|
|
1458
|
+
params.userIds = users;
|
|
1459
|
+
}
|
|
1460
|
+
else if (task === "set-source") {
|
|
1461
|
+
if (!opts.source?.trim())
|
|
1462
|
+
throw new Error("--source is required for set-source");
|
|
1463
|
+
params.source = opts.source.trim();
|
|
1464
|
+
}
|
|
1465
|
+
else if (task === "set-status") {
|
|
1466
|
+
if (!opts.status?.trim())
|
|
1467
|
+
throw new Error("--status is required for set-status");
|
|
1468
|
+
params.status = opts.status.trim();
|
|
1469
|
+
}
|
|
1470
|
+
else if (task === "enroll" && !opts.workflow) {
|
|
1471
|
+
throw new Error("--workflow is required for enroll");
|
|
1472
|
+
}
|
|
1473
|
+
const body = { name: opts.name, operation, ...(task === "enroll" ? { automationId: opts.workflow } : {}), ...(Object.keys(params).length ? { params } : {}), ...(opts.comment ? { comment: opts.comment } : {}) };
|
|
1474
|
+
if (opts.ids) {
|
|
1475
|
+
body.contactIds = csvArg(opts.ids, "--ids");
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
const filter = {};
|
|
1479
|
+
if (typeof opts.search === "string")
|
|
1480
|
+
filter.search = opts.search;
|
|
1481
|
+
if (typeof opts.tag === "string")
|
|
1482
|
+
filter.tag = opts.tag;
|
|
1483
|
+
if (typeof opts.tags === "string")
|
|
1484
|
+
filter.tags = opts.tags;
|
|
1485
|
+
if (typeof opts.filterSource === "string")
|
|
1486
|
+
filter.source = opts.filterSource;
|
|
1487
|
+
if (typeof opts.assignedTo === "string")
|
|
1488
|
+
filter.assigned_to = opts.assignedTo;
|
|
1489
|
+
if (typeof opts.smartView === "string")
|
|
1490
|
+
filter.smart_view = opts.smartView;
|
|
1491
|
+
if (typeof opts.smartList === "string")
|
|
1492
|
+
filter.smartListId = opts.smartList;
|
|
1493
|
+
if (typeof opts.importId === "string")
|
|
1494
|
+
filter.import_id = opts.importId;
|
|
1495
|
+
if (typeof opts.masterStatus === "string")
|
|
1496
|
+
filter.master_status = opts.masterStatus.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1497
|
+
if (typeof opts.filter === "string") {
|
|
1498
|
+
const parsed = jsonArg(opts.filter, "--filter");
|
|
1499
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1500
|
+
if (typeof v === "string" || (Array.isArray(v) && v.every((x) => typeof x === "string")))
|
|
1501
|
+
filter[k] = v;
|
|
1502
|
+
else
|
|
1503
|
+
throw new Error(`--filter.${k} must be a string or an array of strings.`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const hasValue = Object.values(filter).some((v) => (Array.isArray(v) ? v.some((x) => x.trim()) : v.trim()));
|
|
1507
|
+
if (!hasValue)
|
|
1508
|
+
throw new Error("Give --ids, or at least one filter value; a run over every contact is not a bulk action.");
|
|
1509
|
+
const expected = Number(opts.expected);
|
|
1510
|
+
if (!Number.isInteger(expected) || expected < 1)
|
|
1511
|
+
throw new Error("--expected must be the positive whole number the list showed for this filter.");
|
|
1512
|
+
body.filter = filter;
|
|
1513
|
+
body.expectedTotal = expected;
|
|
1514
|
+
}
|
|
1515
|
+
return client.post("/api/v1/batch-operations", body);
|
|
1516
|
+
}));
|
|
1517
|
+
batch
|
|
1518
|
+
.command("list")
|
|
1519
|
+
.description("List Run Batch runs (newest first) with their task, status and progress")
|
|
1520
|
+
.option("--status <status>", "queued | running | completed | cancelled | failed")
|
|
1521
|
+
.option("--page <n>", "page number", "1")
|
|
1522
|
+
.option("--limit <n>", "rows per page (max 50)", "20")
|
|
1523
|
+
.action(run(async (client, opts) => {
|
|
1524
|
+
const qp = new URLSearchParams({ page: opts.page, limit: opts.limit });
|
|
1525
|
+
if (opts.status)
|
|
1526
|
+
qp.set("status", opts.status);
|
|
1527
|
+
return client.get(`/api/v1/batch-operations?${qp.toString()}`);
|
|
1528
|
+
}));
|
|
1529
|
+
batch
|
|
1530
|
+
.command("get <id>")
|
|
1531
|
+
.description("One run with its per-contact items (success / error / skipped, with the reason)")
|
|
1532
|
+
.option("--item-status <status>", "all | pending | success | error | skipped", "all")
|
|
1533
|
+
.option("--page <n>", "item page", "1")
|
|
1534
|
+
.option("--limit <n>", "items per page (max 50)", "20")
|
|
1535
|
+
.action(run(async (client, id, opts) => {
|
|
1536
|
+
assertUuid(id);
|
|
1537
|
+
const qp = new URLSearchParams({ itemStatus: opts.itemStatus, page: opts.page, limit: opts.limit });
|
|
1538
|
+
return client.get(`/api/v1/batch-operations/${id}?${qp.toString()}`);
|
|
1539
|
+
}));
|
|
1540
|
+
batch
|
|
1541
|
+
.command("cancel <id>")
|
|
1542
|
+
.description("Stop a queued or running run; contacts already processed stay as they are (no undo)")
|
|
1543
|
+
.action(run(async (client, id) => {
|
|
1544
|
+
assertUuid(id);
|
|
1545
|
+
return client.post(`/api/v1/batch-operations/${id}/cancel`, {});
|
|
1546
|
+
}));
|
|
1308
1547
|
const companies = program.command("companies").description("Manage companies");
|
|
1309
1548
|
companies
|
|
1310
1549
|
.command("list")
|
package/package.json
CHANGED