conduyt 1.74.0 → 1.76.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/README.md +4 -0
- package/dist/index.js +74 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,6 +88,10 @@ conduyt templates get <id> # one template with its full content (an HTML-mode
|
|
|
88
88
|
conduyt templates create --name "Promo" --channel email --subject "Hi" --body-mode html --html-file email.html # pasted HTML (email only, 102,400 bytes at most); blocks: --body "…" [--body-html …]; 400 codes (html_too_large, html_script, html_conditional_comment, html_office_markup, html_duplicate_attribute, template_corrupt, …) exit 1 verbatim
|
|
89
89
|
conduyt templates update <id> [--name …] [--body-mode html --html-file email.html] [--active false] # partial update
|
|
90
90
|
conduyt dialer history --q "refund" --recorded-only [--disposition X] [--user <id>] [--from D] [--to D] [--mine] # QA search of the call log: per-row `match`, meta.incomplete (a lower-bound total when a number search is capped), stats; 200 per page
|
|
91
|
+
conduyt dialer history --unmatched --line <number> # only calls no contact is linked to, only inbound calls to our line (the unknown-caller counter's list)
|
|
92
|
+
conduyt dialer unknown-callers --line <number> --since <ISO instant> # the counter: { line, since, total, unmatched } (since = the start of day, within 31 days)
|
|
93
|
+
conduyt dialer link-call <callId> --contact <id> [--attach] [--dry-run] # link a contact-less call to a contact; --attach adds its number to the next free phone slot; --dry-run writes nothing
|
|
94
|
+
conduyt contacts create --first X --call <callId> # the lead for a contact-less inbound call, linked in the same transaction (the server sets source Inbound Call and the owner)
|
|
91
95
|
conduyt dialer calls --status voicemail --q "+1555…" # the recordings / voicemail list with the same search (100 per page)
|
|
92
96
|
conduyt dialer recording download <callId> [--voicemail] [-o file] [--force] # stream the call audio to a file (named by the API when no -o); never over an existing file without --force
|
|
93
97
|
conduyt privacy export <id> # GDPR data-portability export (owner/admin)
|
package/dist/index.js
CHANGED
|
@@ -101,9 +101,23 @@ contacts
|
|
|
101
101
|
.command("get <id>")
|
|
102
102
|
.description("Get a single contact by id (includes `revision`: pass it as --expected-revision on `contacts update` / `contacts collaborators`; `hasOutboundSms` is true when the workspace has ever PROVABLY texted the contact: a proven send only, not a draft, a queued row or a pre-send failure; use it to pick `messages check --context`: false = first, true = conversation; `hasOutboundEmail` is the same for email: true when at least one outbound email to the contact was proven sent, so a genuine \"Re:\" reply is a conversation and not flagged)")
|
|
103
103
|
.action(run(async (client, id) => client.get(`/api/v1/contacts/${encodeURIComponent(id)}`)));
|
|
104
|
+
/** An RFC 3339 instant WITH a Z or an explicit offset (an offset-less value would be read in the runtime's or server's timezone),
|
|
105
|
+
* and a real calendar date and time (new Date() would silently roll 2026-02-31 into March). */
|
|
106
|
+
function assertRfc3339Instant(value, label) {
|
|
107
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-](\d{2}):(\d{2}))$/i.exec(value.trim());
|
|
108
|
+
const bad = () => new Error(`${label} must be an RFC 3339 instant with Z or an explicit offset, such as 2026-10-15T07:00:00.000Z or 2026-10-15T00:00:00-07:00 (got ${JSON.stringify(value)})`);
|
|
109
|
+
if (!m)
|
|
110
|
+
throw bad();
|
|
111
|
+
const [, y, mo, d, h, mi, se, , oh, om] = m;
|
|
112
|
+
const dt = new Date(Date.UTC(+y, +mo - 1, +d, +h, +mi, +se));
|
|
113
|
+
if (dt.getUTCFullYear() !== +y || dt.getUTCMonth() !== +mo - 1 || dt.getUTCDate() !== +d || +h > 23 || +mi > 59 || +se > 59)
|
|
114
|
+
throw bad();
|
|
115
|
+
if (oh !== undefined && (+oh > 23 || +om > 59))
|
|
116
|
+
throw bad();
|
|
117
|
+
}
|
|
104
118
|
contacts
|
|
105
119
|
.command("create")
|
|
106
|
-
.description("Create a contact. An impossible US/Canada number (area code or exchange starting 0 or 1) is refused 400 phone_invalid and nothing is saved; the refusal exits 1 with the status, message and code, sent once")
|
|
120
|
+
.description("Create a contact. An impossible US/Canada number (area code or exchange starting 0 or 1) is refused 400 phone_invalid and nothing is saved; the refusal exits 1 with the status, message and code, sent once. --call <callId> creates the lead FOR a contact-less inbound call (the unknown-caller prompt's Create new lead): the call is linked in the same transaction, the phone is the call's own number (omit --phone or pass the same number), --first is required, the account's form-required fields are not required for this lead only, the server derives BOTH the source ('Inbound Call') and the owner (the rep who took the call), so --call is refused together with --source, --assigned-to or a source / assignedTo key in --json (nothing is sent); needs dialer.create (a key also needs the calls scope; a dialer key covers it); 409 call_already_linked, 422 call_not_inbound / number_unusable / phone_not_call_number: nothing is created")
|
|
107
121
|
.option("--first <name>", "first name")
|
|
108
122
|
.option("--last <name>", "last name")
|
|
109
123
|
.option("--email <email>", "email")
|
|
@@ -112,9 +126,15 @@ contacts
|
|
|
112
126
|
.option("--company <company>", "company name (auto-creates/links a company)")
|
|
113
127
|
.option("--source <source>", "lead source (first-write-wins acquisition truth)")
|
|
114
128
|
.option("--master-status <status>", "lead lifecycle status: open|won|lost|abandoned|disqualified or an account custom (terminal statuses silence all outbound automation)")
|
|
129
|
+
.option("--assigned-to <userId>", "owner (user id) of the new lead; refused together with --call (the server assigns the rep who took the call)")
|
|
130
|
+
.option("--call <callId>", "the contact-less inbound call this lead is for (sent as callId; linked in the same transaction); refused together with --source / --assigned-to")
|
|
115
131
|
.option("--json <json>", "full JSON body, merged over the flags above (for any field)")
|
|
116
132
|
.action(run(async (client, opts) => {
|
|
117
133
|
const body = {};
|
|
134
|
+
if (opts.call !== undefined) {
|
|
135
|
+
assertUuid(opts.call, "--call");
|
|
136
|
+
body.callId = opts.call.trim();
|
|
137
|
+
}
|
|
118
138
|
if (opts.first !== undefined)
|
|
119
139
|
body.firstName = opts.first;
|
|
120
140
|
if (opts.last !== undefined)
|
|
@@ -141,8 +161,17 @@ contacts
|
|
|
141
161
|
throw new Error("--master-status must be at most 40 characters.");
|
|
142
162
|
body.masterStatus = opts.masterStatus;
|
|
143
163
|
}
|
|
164
|
+
if (opts.assignedTo !== undefined) {
|
|
165
|
+
if (opts.assignedTo.trim() === "")
|
|
166
|
+
throw new Error("--assigned-to was provided but is blank. Omit it or pass a user id.");
|
|
167
|
+
body.assignedTo = opts.assignedTo.trim();
|
|
168
|
+
}
|
|
144
169
|
if (opts.json !== undefined)
|
|
145
170
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
171
|
+
// checked AFTER flags and --json are merged: the server derives both fields from the call and keeps an explicit assignedTo
|
|
172
|
+
if (body.callId !== undefined && (body.source !== undefined || body.assignedTo !== undefined)) {
|
|
173
|
+
throw new Error("--call cannot be combined with source or assignedTo (--source, --assigned-to, or those keys in --json): for a lead created from a call the server derives both (source 'Inbound Call', owner = the rep who took the call). Omit them.");
|
|
174
|
+
}
|
|
146
175
|
assertContactSource(body.source, { allowNull: false });
|
|
147
176
|
return client.post("/api/v1/contacts", body);
|
|
148
177
|
}));
|
|
@@ -3944,6 +3973,8 @@ function callSearchQuery(opts) {
|
|
|
3944
3973
|
direction: opts.direction,
|
|
3945
3974
|
status: opts.status,
|
|
3946
3975
|
mine: opts.mine ? "true" : undefined,
|
|
3976
|
+
unmatched: opts.unmatched ? "1" : undefined,
|
|
3977
|
+
line: opts.line,
|
|
3947
3978
|
page: opts.page,
|
|
3948
3979
|
per_page: opts.limit,
|
|
3949
3980
|
});
|
|
@@ -3952,6 +3983,8 @@ callSearchOptions(dialer
|
|
|
3952
3983
|
.command("history")
|
|
3953
3984
|
.description(`Search and filter the dialer's call log for QA (GET /dialer/history, newest first, up to 200 per page): find the recorded calls to review by words, number, disposition, agent and dates. ${CALL_SEARCH_HELP}. Also prints stats { total, inbound, outbound, avgTalkSeconds } for the whole visible log`))
|
|
3954
3985
|
.option("--mine", "only the key's own member's calls")
|
|
3986
|
+
.option("--unmatched", "only calls no contact is linked to (the unknown-caller prompt's not-matched calls; sent as unmatched=1)")
|
|
3987
|
+
.option("--line <number>", "our line, any formatting: only INBOUND calls to it (the toll-free counter's link); with --direction outbound it is a 400 line_direction_conflict")
|
|
3955
3988
|
.option("--page <n>", "page number")
|
|
3956
3989
|
.option("--limit <n>", "results per page (per_page, default 50, max 200)")
|
|
3957
3990
|
.action(run(async (client, opts) => ({ data: historyPage(await client.get(`/api/v1/dialer/history${callSearchQuery(opts)}`)) })));
|
|
@@ -3961,6 +3994,34 @@ callSearchOptions(dialer
|
|
|
3961
3994
|
.option("--page <n>", "page number")
|
|
3962
3995
|
.option("--limit <n>", "results per page (per_page, default 30, max 100)")
|
|
3963
3996
|
.action(run(async (client, opts) => ({ data: callsPage(await client.get(`/api/v1/dialer/calls${callSearchQuery(opts)}`)) })));
|
|
3997
|
+
dialer
|
|
3998
|
+
.command("link-call <callId>")
|
|
3999
|
+
.description("Link a contact-less call to an existing contact (POST /calls/{id}/link, the unknown-caller prompt's Search a contact + Attach). --attach also adds the call's number to the contact's next free phone slot (the empty primary, then phone_2, phone_3 ...; nothing is overwritten; already there = no write). --dry-run answers what the link would do (attached.slot, alreadyOnContact, alreadyLinked, or the same refusal) and writes nothing. Refusals write nothing and exit 1 with the status, message and code: 409 call_already_linked / no_free_phone_slot / number_on_another_contact / number_owner_undecidable (too many contacts share the number's last seven digits; link without --attach), 403 contact_not_editable, 404 call_not_found / contact_not_found, 422 number_unusable. A repeat is idempotent (alreadyLinked: true). To create a new lead from the call instead: contacts create --call <callId>. Needs dialer.create, plus contacts.edit with --attach")
|
|
4000
|
+
.requiredOption("--contact <id>", "the contact to link the call to (UUID)")
|
|
4001
|
+
.option("--attach", "also add the call's number to the contact's next free phone slot")
|
|
4002
|
+
.option("--dry-run", "answer what the request would do; write nothing")
|
|
4003
|
+
.action(run(async (client, callId, opts) => {
|
|
4004
|
+
assertUuid(callId, "call id");
|
|
4005
|
+
assertUuid(opts.contact, "--contact");
|
|
4006
|
+
const body = { contactId: opts.contact.trim() };
|
|
4007
|
+
if (opts.attach)
|
|
4008
|
+
body.attachNumber = true;
|
|
4009
|
+
if (opts.dryRun)
|
|
4010
|
+
body.dryRun = true;
|
|
4011
|
+
return client.post(`/api/v1/calls/${encodeURIComponent(callId.trim())}/link`, body);
|
|
4012
|
+
}));
|
|
4013
|
+
dialer
|
|
4014
|
+
.command("unknown-callers")
|
|
4015
|
+
.description("Count a line's unknown callers since an instant (GET /calls/unknown-callers, the toll-free quick mode counter: N unknown callers today, M not matched yet). Prints { line, since, total, unmatched }: total = inbound calls to that line since --since that arrived with no contact (still contact-less plus the ones the unknown-caller prompt linked since), unmatched = those still contact-less (list them with `dialer history --unmatched --line <number>`). --since is the caller's start of day as an ISO instant and must be within the last 31 days. Refusals exit 1 with the API's code: line_required, line_conflict, line_invalid, since_required, since_conflict, since_out_of_range. Read-only, counts only; needs dialer.view (a scoped member counts their own calls, contact-less calls and calls linked to a contact they can see)")
|
|
4016
|
+
.requiredOption("--line <number>", "our line (the number the callers dialed), any formatting")
|
|
4017
|
+
.requiredOption("--since <instant>", "ISO instant, the caller's midnight, e.g. 2026-10-15T07:00:00.000Z (within the last 31 days)")
|
|
4018
|
+
.action(run(async (client, opts) => {
|
|
4019
|
+
if (!opts.line.trim())
|
|
4020
|
+
throw new Error("--line is required");
|
|
4021
|
+
const since = opts.since.trim();
|
|
4022
|
+
assertRfc3339Instant(since, "--since");
|
|
4023
|
+
return client.get(`/api/v1/calls/unknown-callers${buildQuery({ line: opts.line.trim(), since })}`);
|
|
4024
|
+
}));
|
|
3964
4025
|
dialerRecording
|
|
3965
4026
|
.command("download <callId>")
|
|
3966
4027
|
.description("Download a call's audio to a file (GET /dialer/call/{id}/media?type=recording&download=1): the SAME authorisation and stream as playing it, streamed to disk. Saved as -o <file>, else under the name the API gives it (<Contact or number>_<date>_<time>_<callId8>.mp3) in the current folder; never over an existing file unless --force. --voicemail downloads the voicemail instead (type=voicemail). Prints { saved, bytes, contentType }. A call with no media is a 404, a call you may not see is a 404, an unusable media host is a 422 (exit 1 with the API's message; nothing is written)")
|
|
@@ -4027,16 +4088,17 @@ dialer
|
|
|
4027
4088
|
}));
|
|
4028
4089
|
dialer
|
|
4029
4090
|
.command("settings")
|
|
4030
|
-
.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.")
|
|
4091
|
+
.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), wrapupAutoReleaseSeconds (seconds of continuous wrap-up after which a rep returns to Available automatically; 0 = off, otherwise 30 to 3600; default 300), plus localPresenceConfigured.")
|
|
4031
4092
|
.action(run(async (client) => client.get("/api/v1/dialer/settings")));
|
|
4032
4093
|
dialer
|
|
4033
4094
|
.command("set-settings")
|
|
4034
|
-
.description("Change the account's Smart Dialing policy (PATCH /dialer/settings, settings scope). Pass only what changes: --auto-advance on|off, --pause-seconds 1-60, --advance-on disposition|timer, --open-lead on|off (the lead's record opens as each Smart Dialing call goes out), --calls-from local_presence|did.")
|
|
4095
|
+
.description("Change the account's Smart Dialing policy (PATCH /dialer/settings, settings scope). Pass only what changes: --auto-advance on|off, --pause-seconds 1-60, --advance-on disposition|timer, --open-lead on|off (the lead's record opens as each Smart Dialing call goes out), --calls-from local_presence|did, --wrapup-seconds 0|30-3600 (seconds of continuous wrap-up after which a rep returns to Available automatically; 0 = off, otherwise 30-3600).")
|
|
4035
4096
|
.option("--auto-advance <on|off>", "start the next dial automatically after a call")
|
|
4036
4097
|
.option("--pause-seconds <n>", "seconds between calls when auto-advance is on (1-60)")
|
|
4037
4098
|
.option("--advance-on <mode>", "disposition | timer")
|
|
4038
4099
|
.option("--open-lead <on|off>", "open the lead's record as each Smart Dialing call goes out")
|
|
4039
4100
|
.option("--calls-from <line>", "local_presence | did")
|
|
4101
|
+
.option("--wrapup-seconds <n>", "seconds of continuous wrap-up after which a rep returns to Available automatically: 0 = off, otherwise 30-3600")
|
|
4040
4102
|
.action(run(async (client, opts) => {
|
|
4041
4103
|
const body = {};
|
|
4042
4104
|
if (opts.autoAdvance !== undefined)
|
|
@@ -4061,8 +4123,16 @@ dialer
|
|
|
4061
4123
|
throw new Error(`--calls-from must be one of ${SMART_DIAL_CALLER_IDS.join(", ")}.`);
|
|
4062
4124
|
body.smartDialCallerId = line;
|
|
4063
4125
|
}
|
|
4126
|
+
if (opts.wrapupSeconds !== undefined) {
|
|
4127
|
+
// digits only: Number() would read "" as 0 (switching the timer off) and "1e2" / "0x1e" / "30.0" as numbers
|
|
4128
|
+
const raw = opts.wrapupSeconds.trim();
|
|
4129
|
+
const n = /^\d{1,5}$/.test(raw) ? Number(raw) : NaN;
|
|
4130
|
+
if (!(n === 0 || (n >= 30 && n <= 3600)))
|
|
4131
|
+
throw new Error("--wrapup-seconds must be 0 (off) or a whole number of seconds from 30 to 3600.");
|
|
4132
|
+
body.wrapupAutoReleaseSeconds = n;
|
|
4133
|
+
}
|
|
4064
4134
|
if (Object.keys(body).length === 0)
|
|
4065
|
-
throw new Error("Nothing to change: pass at least one of --auto-advance, --pause-seconds, --advance-on, --open-lead, --calls-from.");
|
|
4135
|
+
throw new Error("Nothing to change: pass at least one of --auto-advance, --pause-seconds, --advance-on, --open-lead, --calls-from, --wrapup-seconds.");
|
|
4066
4136
|
return client.patch("/api/v1/dialer/settings", body);
|
|
4067
4137
|
}));
|
|
4068
4138
|
dialer
|
package/package.json
CHANGED