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.
Files changed (2) hide show
  1. package/dist/index.js +239 -0
  2. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.43.0",
3
+ "version": "1.45.0",
4
4
  "description": "Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",