@erdoai/cli 0.101.1 → 0.103.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 +195 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -609,6 +609,25 @@ var ErdoClient = class {
|
|
|
609
609
|
`/v1/voice/phone-numbers/${encodeURIComponent(number)}/sms`
|
|
610
610
|
);
|
|
611
611
|
}
|
|
612
|
+
// The organization's caller name and what each number shows when it calls.
|
|
613
|
+
// Read from Erdo's own record; nothing here reaches the provider.
|
|
614
|
+
getVoiceCallerName() {
|
|
615
|
+
return this.request("GET", "/v1/voice/caller-name");
|
|
616
|
+
}
|
|
617
|
+
// Look the numbers up now — one carrier lookup, about a cent, per number.
|
|
618
|
+
// Org admins only; a number looked up in the last minute is skipped.
|
|
619
|
+
checkVoiceCallerName(number) {
|
|
620
|
+
return this.request(
|
|
621
|
+
"POST",
|
|
622
|
+
"/v1/voice/caller-name/lookup",
|
|
623
|
+
number ? { phone_number: number } : {}
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
// Registering the caller name puts the organization's name in front of the
|
|
627
|
+
// carriers, so this files the same approval-gated action the chat agent files.
|
|
628
|
+
provisionVoiceCallerName() {
|
|
629
|
+
return this.request("POST", "/v1/voice/caller-name");
|
|
630
|
+
}
|
|
612
631
|
// Turn an agent's answering of texts on or off. Only the switch changes: the
|
|
613
632
|
// agent's persona, number and calls are untouched, and texts keep arriving and
|
|
614
633
|
// stay readable either way.
|
|
@@ -2421,14 +2440,20 @@ function printBusinessProfile(profile) {
|
|
|
2421
2440
|
["job position", profile.representative_job_position],
|
|
2422
2441
|
["email", profile.representative_email],
|
|
2423
2442
|
["phone", profile.representative_phone],
|
|
2443
|
+
["caller name", profile.caller_name || "not set"],
|
|
2424
2444
|
["updated", profile.updated_at ?? ""]
|
|
2425
2445
|
]
|
|
2426
2446
|
);
|
|
2427
2447
|
}
|
|
2428
|
-
function printMissingProfileFields(missing) {
|
|
2429
|
-
if (missing.length === 0) return;
|
|
2448
|
+
function printMissingProfileFields(missing, callerNameMissing = []) {
|
|
2449
|
+
if (missing.length === 0 && callerNameMissing.length === 0) return;
|
|
2430
2450
|
console.log("");
|
|
2431
|
-
|
|
2451
|
+
if (missing.length > 0) {
|
|
2452
|
+
console.log(`Still needed before a number can be registered for SMS: ${missing.join(", ")}`);
|
|
2453
|
+
}
|
|
2454
|
+
if (callerNameMissing.length > 0) {
|
|
2455
|
+
console.log(`Still needed before the caller name can be registered: ${callerNameMissing.join(", ")}`);
|
|
2456
|
+
}
|
|
2432
2457
|
console.log("Fill them in with: erdo org business-profile set --help");
|
|
2433
2458
|
}
|
|
2434
2459
|
businessProfileCmd.command("get").description("Show the stored business profile and what SMS registration still needs").option(
|
|
@@ -2450,7 +2475,7 @@ businessProfileCmd.command("get").description("Show the stored business profile
|
|
|
2450
2475
|
return;
|
|
2451
2476
|
}
|
|
2452
2477
|
printBusinessProfile(res.profile);
|
|
2453
|
-
printMissingProfileFields(res.missing_fields);
|
|
2478
|
+
printMissingProfileFields(res.missing_fields, res.caller_name_missing_fields ?? []);
|
|
2454
2479
|
} catch (e) {
|
|
2455
2480
|
fail(e);
|
|
2456
2481
|
}
|
|
@@ -2473,7 +2498,10 @@ businessProfileCmd.command("set").description("Save the organization's business
|
|
|
2473
2498
|
).option("--first-name <name>", "given name of the person carriers may contact").option("--last-name <name>", "family name of that person").option("--title <title>", "their job title in the business's own words, e.g. Managing Partner").option(
|
|
2474
2499
|
"--job-position <position>",
|
|
2475
2500
|
"their role from the fixed list (ceo, cfo, director, general_counsel, gm, vp, other)"
|
|
2476
|
-
).option("--email <email>", "their email address").option("--phone <number>", "their phone number in E.164, e.g. +13055550123").option(
|
|
2501
|
+
).option("--email <email>", "their email address").option("--phone <number>", "their phone number in E.164, e.g. +13055550123").option(
|
|
2502
|
+
"--caller-name <name>",
|
|
2503
|
+
"the caller name (CNAM) every number shows when it calls: at most 15 characters, starting with a letter; letters, digits, spaces, periods and commas; the brand people recognise, not the legal name. Register it with: erdo voice caller-name provision"
|
|
2504
|
+
).option("--json", "print the raw JSON result instead of a table").action(
|
|
2477
2505
|
async (opts) => {
|
|
2478
2506
|
try {
|
|
2479
2507
|
const api = new ErdoClient();
|
|
@@ -2514,7 +2542,9 @@ businessProfileCmd.command("set").description("Save the organization's business
|
|
|
2514
2542
|
representative_title: opts.title ?? stored.representative_title,
|
|
2515
2543
|
representative_job_position: opts.jobPosition ?? stored.representative_job_position,
|
|
2516
2544
|
representative_email: opts.email ?? stored.representative_email,
|
|
2517
|
-
representative_phone: opts.phone ?? stored.representative_phone
|
|
2545
|
+
representative_phone: opts.phone ?? stored.representative_phone,
|
|
2546
|
+
// Absent keeps the stored name server-side, so an unset flag sends none.
|
|
2547
|
+
caller_name: opts.callerName
|
|
2518
2548
|
};
|
|
2519
2549
|
const res = await api.setBusinessProfile(body);
|
|
2520
2550
|
if (opts.json) {
|
|
@@ -2523,7 +2553,7 @@ businessProfileCmd.command("set").description("Save the organization's business
|
|
|
2523
2553
|
}
|
|
2524
2554
|
console.log("Business profile saved.");
|
|
2525
2555
|
printBusinessProfile(res.profile);
|
|
2526
|
-
printMissingProfileFields(res.missing_fields);
|
|
2556
|
+
printMissingProfileFields(res.missing_fields, res.caller_name_missing_fields ?? []);
|
|
2527
2557
|
} catch (e) {
|
|
2528
2558
|
fail(e);
|
|
2529
2559
|
}
|
|
@@ -5894,7 +5924,7 @@ contact: ${parts.join(" \xB7 ")}`);
|
|
|
5894
5924
|
}
|
|
5895
5925
|
}
|
|
5896
5926
|
);
|
|
5897
|
-
var voiceNumbersCmd = voiceCmd.command("numbers").description("The organization's phone numbers
|
|
5927
|
+
var voiceNumbersCmd = voiceCmd.command("numbers").description("The organization's phone numbers, their A2P SMS registration and their caller name");
|
|
5898
5928
|
function printRegistrationState(registration) {
|
|
5899
5929
|
if (registration === null || typeof registration !== "object") {
|
|
5900
5930
|
if (registration !== void 0) print(registration);
|
|
@@ -5942,7 +5972,7 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
5942
5972
|
} else {
|
|
5943
5973
|
const callable = new Set(res.person_call_numbers ?? []);
|
|
5944
5974
|
printAlignedTable(
|
|
5945
|
-
["number", "agent", "kind", "calls", "status", "waiting on", "reason"],
|
|
5975
|
+
["number", "agent", "kind", "calls", "status", "waiting on", "reason", "caller name", "shows"],
|
|
5946
5976
|
numbers.map((n) => [
|
|
5947
5977
|
n.address,
|
|
5948
5978
|
n.agent_ref ?? "",
|
|
@@ -5950,7 +5980,9 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
5950
5980
|
callable.has(n.address) ? "yes" : "",
|
|
5951
5981
|
n.status,
|
|
5952
5982
|
n.next_step ?? "",
|
|
5953
|
-
n.reason ?? ""
|
|
5983
|
+
n.reason ?? "",
|
|
5984
|
+
n.caller_name_attachment ?? "",
|
|
5985
|
+
describeObservedCallerName(n.caller_name_observed, n.observed_caller_name)
|
|
5954
5986
|
])
|
|
5955
5987
|
);
|
|
5956
5988
|
process.stderr.write(`showing ${numbers.length} number(s)
|
|
@@ -5964,6 +5996,7 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
5964
5996
|
);
|
|
5965
5997
|
console.log("Complete it with: erdo org business-profile set --help, or in Settings \u2192 Business.");
|
|
5966
5998
|
}
|
|
5999
|
+
printStaleCallerNameNote(numbers);
|
|
5967
6000
|
} catch (e) {
|
|
5968
6001
|
fail(e);
|
|
5969
6002
|
}
|
|
@@ -6013,6 +6046,126 @@ voiceNumbersCmd.command("provision-sms <number>").description("Ask for A2P SMS r
|
|
|
6013
6046
|
fail(e);
|
|
6014
6047
|
}
|
|
6015
6048
|
});
|
|
6049
|
+
function describeObservedCallerName(observed, name) {
|
|
6050
|
+
switch (observed) {
|
|
6051
|
+
case "ours":
|
|
6052
|
+
return `\u2713 ${name ?? ""}`.trim();
|
|
6053
|
+
case "other":
|
|
6054
|
+
return `${name ?? ""} \u2014 not your business`;
|
|
6055
|
+
case "none":
|
|
6056
|
+
return "(no name)";
|
|
6057
|
+
default:
|
|
6058
|
+
return "";
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
function printStaleCallerNameNote(numbers) {
|
|
6062
|
+
const stale = numbers.filter((n) => (n.caller_name_observed ?? n.observed) === "other");
|
|
6063
|
+
if (stale.length === 0) return;
|
|
6064
|
+
console.log("");
|
|
6065
|
+
for (const n of stale) {
|
|
6066
|
+
console.log(
|
|
6067
|
+
`${n.address ?? n.phone_number ?? "a number"} calls out as "${n.observed_caller_name ?? n.observed_name ?? ""}", which is not this business.`
|
|
6068
|
+
);
|
|
6069
|
+
}
|
|
6070
|
+
console.log("Register the organization's caller name with: erdo voice caller-name provision");
|
|
6071
|
+
console.log("Check what a number shows now (about $0.01 each): erdo voice caller-name check [number]");
|
|
6072
|
+
}
|
|
6073
|
+
var voiceCallerNameCmd = voiceCmd.command("caller-name").description("The name US carriers show when the organization's numbers call");
|
|
6074
|
+
function printCallerNameStatus(status) {
|
|
6075
|
+
printAlignedTable(
|
|
6076
|
+
["field", "value"],
|
|
6077
|
+
[
|
|
6078
|
+
["declared name", status.caller_name || "not set \u2014 erdo org business-profile set --caller-name <name>"],
|
|
6079
|
+
["registered name", status.registered_name ?? ""],
|
|
6080
|
+
["status", status.status],
|
|
6081
|
+
["waiting on", status.next_step ?? ""],
|
|
6082
|
+
["reason", status.reason ?? status.not_registerable_reason ?? ""]
|
|
6083
|
+
]
|
|
6084
|
+
);
|
|
6085
|
+
if (status.numbers.length > 0) {
|
|
6086
|
+
console.log("");
|
|
6087
|
+
printAlignedTable(
|
|
6088
|
+
["number", "agent", "attachment", "shows", "last checked"],
|
|
6089
|
+
status.numbers.map((n) => [
|
|
6090
|
+
n.phone_number,
|
|
6091
|
+
n.agent_slug ?? "",
|
|
6092
|
+
n.registerable ? n.attachment : `not registerable \u2014 ${n.not_registerable_reason ?? ""}`,
|
|
6093
|
+
describeObservedCallerName(n.observed, n.observed_name),
|
|
6094
|
+
n.last_checked_at ? `${n.last_checked_at}${n.last_check_failed ? " (failed)" : ""}` : ""
|
|
6095
|
+
])
|
|
6096
|
+
);
|
|
6097
|
+
}
|
|
6098
|
+
if (status.caller_name_out_of_date) {
|
|
6099
|
+
console.log("");
|
|
6100
|
+
console.log(
|
|
6101
|
+
`The caller name was changed after it was registered: carriers still show ${status.registered_name}. Replace it with: erdo voice caller-name provision (it deletes the current registration and re-enters review).`
|
|
6102
|
+
);
|
|
6103
|
+
}
|
|
6104
|
+
if (status.missing_profile_fields && status.missing_profile_fields.length > 0) {
|
|
6105
|
+
console.log("");
|
|
6106
|
+
console.log(`The business profile still needs: ${status.missing_profile_fields.join(", ")}`);
|
|
6107
|
+
console.log("Complete it with: erdo org business-profile set --help, or in Settings \u2192 Business.");
|
|
6108
|
+
}
|
|
6109
|
+
printStaleCallerNameNote(status.numbers);
|
|
6110
|
+
}
|
|
6111
|
+
voiceCallerNameCmd.command("get").description("Show the caller name, its registration, and what each number shows when it calls").option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
|
|
6112
|
+
try {
|
|
6113
|
+
const res = await new ErdoClient().getVoiceCallerName();
|
|
6114
|
+
if (opts.json) {
|
|
6115
|
+
print(res);
|
|
6116
|
+
return;
|
|
6117
|
+
}
|
|
6118
|
+
printCallerNameStatus(res);
|
|
6119
|
+
} catch (e) {
|
|
6120
|
+
fail(e);
|
|
6121
|
+
}
|
|
6122
|
+
});
|
|
6123
|
+
voiceCallerNameCmd.command("check [number]").description(
|
|
6124
|
+
"Look the numbers up now to see what they show when they call \u2014 about $0.01 per number (admins only; a number checked in the last minute is skipped)"
|
|
6125
|
+
).option("--json", "print the raw JSON result instead of a table").action(async (number, opts) => {
|
|
6126
|
+
try {
|
|
6127
|
+
const res = await new ErdoClient().checkVoiceCallerName(number);
|
|
6128
|
+
if (opts.json) {
|
|
6129
|
+
print(res);
|
|
6130
|
+
return;
|
|
6131
|
+
}
|
|
6132
|
+
console.log(
|
|
6133
|
+
`Checked ${res.checked.length} number(s)` + (res.failed.length > 0 ? `, ${res.failed.length} lookup(s) failed` : "") + (res.skipped.length > 0 ? `, skipped ${res.skipped.length}` : "") + "."
|
|
6134
|
+
);
|
|
6135
|
+
for (const skip of res.skipped) console.log(` skipped ${skip.phone_number}: ${skip.reason}`);
|
|
6136
|
+
for (const failed of res.failed) console.log(` lookup failed for ${failed} \u2014 try again later`);
|
|
6137
|
+
console.log("");
|
|
6138
|
+
printCallerNameStatus(res.caller_name);
|
|
6139
|
+
} catch (e) {
|
|
6140
|
+
fail(e);
|
|
6141
|
+
}
|
|
6142
|
+
});
|
|
6143
|
+
voiceCallerNameCmd.command("provision").description("Register the organization's caller name for every number it holds (approval-gated, free)").option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
|
|
6144
|
+
try {
|
|
6145
|
+
const res = await new ErdoClient().provisionVoiceCallerName();
|
|
6146
|
+
if (opts.json) {
|
|
6147
|
+
print(res);
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
if (res.status === "pending_approval") {
|
|
6151
|
+
console.log("Nothing has been registered yet \u2014 an approval card was filed and awaits a decision.");
|
|
6152
|
+
if (res.action_display) console.log(`action: ${res.action_display}`);
|
|
6153
|
+
if (res.approval_request_id) {
|
|
6154
|
+
console.log(`approval request: ${res.approval_request_id}`);
|
|
6155
|
+
console.log(` erdo approvals show ${res.approval_request_id}`);
|
|
6156
|
+
console.log(` erdo approvals decide ${res.approval_request_id} --approve`);
|
|
6157
|
+
}
|
|
6158
|
+
} else {
|
|
6159
|
+
console.log(`Caller name ${res.status}.`);
|
|
6160
|
+
if (res.registration !== void 0) print(res.registration);
|
|
6161
|
+
}
|
|
6162
|
+
process.stderr.write(
|
|
6163
|
+
"Twilio reviews the name, then it takes 48\u201372 hours to appear; re-read it with: erdo voice caller-name get\n"
|
|
6164
|
+
);
|
|
6165
|
+
} catch (e) {
|
|
6166
|
+
fail(e);
|
|
6167
|
+
}
|
|
6168
|
+
});
|
|
6016
6169
|
var voiceAgentsCmd = voiceCmd.command("agents").description("List the organization's concierges and change settings on one");
|
|
6017
6170
|
voiceAgentsCmd.command("list", { isDefault: true }).description(
|
|
6018
6171
|
"List the organization's concierges: the voice agents that answer its website widget, its phone and its texts. The id is what the lead playbook stores as its linked concierge (`erdo leads playbook set --agent-id`). Archived agents are not listed."
|
|
@@ -6366,20 +6519,39 @@ datasetsCmd.command("from-integration <app>").description("Create a dataset back
|
|
|
6366
6519
|
fail(e);
|
|
6367
6520
|
}
|
|
6368
6521
|
});
|
|
6369
|
-
datasetsCmd.command("configure-integration <dataset-id>").description(
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6522
|
+
datasetsCmd.command("configure-integration <dataset-id>").description(
|
|
6523
|
+
"Set an integration dataset's segment scope and enable canonical sync, or --disable-sync to retire a superseded/stale synced dataset (tears down the warehouse copy and stops it being read or monitored; reversible with --enable-sync)"
|
|
6524
|
+
).option("-s, --segments <csv>", "segment names or ids (see: erdo integrations tables <app>)").option(
|
|
6525
|
+
"--enable-sync",
|
|
6526
|
+
"enable canonical data-platform sync; on its own it restores a retired sync and keeps the saved segment scope"
|
|
6527
|
+
).option(
|
|
6528
|
+
"--disable-sync",
|
|
6529
|
+
"tear down the canonical data-platform sync \u2014 retire a superseded or stale synced dataset; ignores --segments"
|
|
6530
|
+
).action(
|
|
6531
|
+
async (datasetID, opts) => {
|
|
6532
|
+
try {
|
|
6533
|
+
if (opts.enableSync && opts.disableSync) {
|
|
6534
|
+
fail("Pass either --enable-sync or --disable-sync, not both");
|
|
6535
|
+
return;
|
|
6536
|
+
}
|
|
6537
|
+
if (!opts.disableSync && !opts.enableSync && !opts.segments) {
|
|
6538
|
+
fail("--segments is required unless you pass --enable-sync or --disable-sync");
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
const segments = (opts.segments ?? "").split(",").map((segment) => segment.trim()).filter(Boolean);
|
|
6542
|
+
const result = await new ErdoClient().configureIntegrationDataset(datasetID, {
|
|
6543
|
+
segments,
|
|
6544
|
+
enable_sync: opts.enableSync,
|
|
6545
|
+
disable_sync: opts.disableSync
|
|
6546
|
+
});
|
|
6547
|
+
console.log(
|
|
6548
|
+
opts.disableSync ? `Retired sync for dataset ${result.dataset_id} (status: ${result.status}). Re-enable with --enable-sync.` : `Configured dataset ${result.dataset_id}, status: ${result.status}, sync: ${result.sync_enabled ? "enabled" : "unchanged"}`
|
|
6549
|
+
);
|
|
6550
|
+
} catch (e) {
|
|
6551
|
+
fail(e);
|
|
6552
|
+
}
|
|
6381
6553
|
}
|
|
6382
|
-
|
|
6554
|
+
);
|
|
6383
6555
|
var refreshCmd = datasetsCmd.command("refresh").description(
|
|
6384
6556
|
"The recipe that keeps a dataset current, and the triggers that run it \u2014 read it with `show`, install or replace it with `set`, and run it once now with `run`"
|
|
6385
6557
|
);
|