conduyt 1.56.0 → 1.58.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 (3) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +355 -2
  3. 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 also stops the contact's sequences and drips); 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>")
@@ -2413,6 +2682,90 @@ function onOffArg(raw, flag) {
2413
2682
  return false;
2414
2683
  throw new Error(`${flag} takes on or off (got '${raw}').`);
2415
2684
  }
2685
+ /** the two lines a workspace records calls on: its Twilio lines (settings.recording) and its iPhone line (settings.projectBlueRecording) */
2686
+ const RECORDING_KEY = { twilio: "recording", iphone: "projectBlueRecording" };
2687
+ const isDisclosureOn = (v) => v === "inbound" || v === "outbound" || v === "both";
2688
+ /** what a stored policy MEANS, read the way the server reads it: the old shape { enabled, consentGreeting } is outbound
2689
+ * recording with the disclosure on whenever a wording is set; enabled stays beside outbound as the old name of the switch */
2690
+ export function effectiveRecordingPolicy(raw) {
2691
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
2692
+ return null;
2693
+ const r = raw;
2694
+ const greeting = typeof r.consentGreeting === "string" ? r.consentGreeting.trim() : "";
2695
+ const newShape = typeof r.inbound === "boolean" || typeof r.outbound === "boolean" || (r.disclosure !== undefined && r.disclosure !== null);
2696
+ if (newShape) {
2697
+ const d = (r.disclosure && typeof r.disclosure === "object" ? r.disclosure : {});
2698
+ const outbound = typeof r.outbound === "boolean" ? r.outbound : r.enabled === true;
2699
+ return { inbound: r.inbound === true, outbound, disclosure: { enabled: d.enabled === true, on: isDisclosureOn(d.on) ? d.on : "outbound" }, consentGreeting: greeting, enabled: outbound };
2700
+ }
2701
+ const outbound = r.enabled === true;
2702
+ return { inbound: false, outbound, disclosure: { enabled: outbound && greeting.length > 0, on: "outbound" }, consentGreeting: greeting, enabled: outbound };
2703
+ }
2704
+ /** the iPhone line has no inbound calls: what its policy does there, the way the server applies it */
2705
+ export function iphoneRecordingDomain(policy) {
2706
+ return { ...policy, inbound: false, disclosure: policy.disclosure.on === "inbound" ? { enabled: false, on: "outbound" } : { enabled: policy.disclosure.enabled, on: "outbound" } };
2707
+ }
2708
+ dialer
2709
+ .command("recording")
2710
+ .description("Show the workspace's call recording policy for both kinds of line (GET /settings). twilio = its Twilio lines: always an object, with everything off when the workspace never set it. Needs a key whose creator may view settings; without it the command fails instead of showing a policy. iphone = the iPhone line's OWN policy (outbound only); null = none was written, and the iPhone line then follows the Twilio lines' policy without its inbound part. Each object is the policy as it is APPLIED: inbound, outbound, disclosure { enabled, on: inbound | outbound | both }, consentGreeting (what is said; empty = the default sentence) and enabled (the OLD name of the outbound switch: the same value as outbound). A workspace that never saved the new shape is read the way the server reads its old one: outbound recording with the disclosure on whenever a wording is set")
2711
+ .action(run(async (client) => {
2712
+ const result = (await client.get("/api/v1/settings"));
2713
+ const raw = result?.data ?? {};
2714
+ const twilio = effectiveRecordingPolicy(raw[RECORDING_KEY.twilio]);
2715
+ // a key whose creator may not view settings gets a short answer WITHOUT the policy: that is "not visible",
2716
+ // never "not configured" (the full answer always carries the Twilio policy)
2717
+ if (!twilio)
2718
+ throw new Error("The call recording policy is not visible to this API key: its creator needs permission to view settings. Nothing can be said about whether calls are recorded.");
2719
+ const iphone = effectiveRecordingPolicy(raw[RECORDING_KEY.iphone]);
2720
+ return { data: { twilio, iphone: iphone ? iphoneRecordingDomain(iphone) : null } };
2721
+ }));
2722
+ dialer
2723
+ .command("set-recording")
2724
+ .description("Change the workspace's call recording policy (admin: settings scope). Send only what changes; the rest stays. The workspace is responsible for consent where it operates. Example: conduyt dialer set-recording --inbound on --outbound on --disclosure on --disclosure-on inbound")
2725
+ .option("--line <line>", "which line's policy: twilio (default) or iphone")
2726
+ .option("--inbound <on|off>", "record calls the team receives (Twilio lines only)")
2727
+ .option("--outbound <on|off>", "record calls the team makes, including Smart Dialing")
2728
+ .option("--enabled <on|off>", "the OLD name of --outbound, accepted for older callers and sent as --outbound (when both are sent, --outbound decides); nothing else changes with it")
2729
+ .option("--disclosure <on|off>", "play a recording disclosure (send together with --disclosure-on)")
2730
+ .option("--disclosure-on <calls>", "which calls the disclosure plays on: inbound | outbound | both (send together with --disclosure). On the iPhone line: inbound is refused, both is saved as outbound")
2731
+ .option("--greeting <text>", "what is said (500 characters at most; an empty string = the default sentence)")
2732
+ .action(run(async (client, opts) => {
2733
+ const line = opts.line ?? "twilio";
2734
+ if (line !== "twilio" && line !== "iphone")
2735
+ throw new Error("--line must be twilio or iphone.");
2736
+ if ([opts.inbound, opts.outbound, opts.enabled, opts.disclosure, opts.disclosureOn, opts.greeting].every((v) => v === undefined)) {
2737
+ throw new Error("Nothing to change: pass at least one of --inbound, --outbound, --disclosure with --disclosure-on, --greeting.");
2738
+ }
2739
+ if ((opts.disclosure === undefined) !== (opts.disclosureOn === undefined)) {
2740
+ throw new Error("--disclosure and --disclosure-on are sent together: the disclosure is one setting (whether it plays, and on which calls).");
2741
+ }
2742
+ if (line === "iphone" && opts.inbound !== undefined)
2743
+ throw new Error("The iPhone line records outbound calls only: --inbound is not a setting of that line.");
2744
+ const policy = {};
2745
+ if (opts.inbound !== undefined)
2746
+ policy.inbound = onOffArg(opts.inbound, "--inbound");
2747
+ // (Codex r4) the old name is the outbound switch and nothing else: sent AS outbound, never as enabled (the server
2748
+ // gives an enabled-only write over a policy saved in the old shape its old whole meaning, disclosure included)
2749
+ if (opts.outbound !== undefined)
2750
+ policy.outbound = onOffArg(opts.outbound, "--outbound");
2751
+ else if (opts.enabled !== undefined)
2752
+ policy.outbound = onOffArg(opts.enabled, "--enabled");
2753
+ if (opts.disclosure !== undefined && opts.disclosureOn !== undefined) {
2754
+ if (!["inbound", "outbound", "both"].includes(opts.disclosureOn))
2755
+ throw new Error("--disclosure-on must be one of: inbound, outbound, both.");
2756
+ // the iPhone line has no inbound calls: a disclosure on inbound only would never play, so it is refused here
2757
+ // instead of saved as if it were on; both is what the line does on its outbound calls
2758
+ if (line === "iphone" && opts.disclosureOn === "inbound")
2759
+ throw new Error("The iPhone line has outbound calls only: a disclosure on inbound calls would never play. Use --disclosure-on outbound.");
2760
+ policy.disclosure = { enabled: onOffArg(opts.disclosure, "--disclosure"), on: line === "iphone" && opts.disclosureOn === "both" ? "outbound" : opts.disclosureOn };
2761
+ }
2762
+ if (opts.greeting !== undefined) {
2763
+ if (opts.greeting.trim().length > 500)
2764
+ throw new Error("--greeting may be at most 500 characters.");
2765
+ policy.consentGreeting = opts.greeting.trim();
2766
+ }
2767
+ return patchGeneralSettings(client, { [RECORDING_KEY[line]]: policy });
2768
+ }));
2416
2769
  dialer
2417
2770
  .command("settings")
2418
2771
  .description("Show the account's Smart Dialing policy (GET /dialer/settings, dialer scope): autoAdvanceEnabled, pauseSeconds, advanceOn (disposition | timer), openLeadOnDial (the lead's record opens as each Smart Dialing call goes out), smartDialCallerId (local_presence = an area-code match from the number pool, the rep's own line when no pool exists; did = always the rep's own line), plus localPresenceConfigured.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.56.0",
3
+ "version": "1.58.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",