@erdoai/cli 0.143.0 → 0.145.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 +204 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ function describeRefreshWrites(strategy, mode, key, liveMode) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// src/index.ts
|
|
53
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
53
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
54
54
|
import { basename } from "path";
|
|
55
55
|
import { password, select } from "@inquirer/prompts";
|
|
56
56
|
import { Command as Command2, Option as Option2 } from "commander";
|
|
@@ -275,6 +275,17 @@ var ErdoClient = class {
|
|
|
275
275
|
input
|
|
276
276
|
);
|
|
277
277
|
}
|
|
278
|
+
listPaidMediaCampaigns(customerId) {
|
|
279
|
+
const query = customerId ? `?customer_id=${encodeURIComponent(customerId)}` : "";
|
|
280
|
+
return this.request("GET", `/v1/paid-media/campaigns${query}`);
|
|
281
|
+
}
|
|
282
|
+
setPaidMediaCampaignConversionGoals(campaignId, input) {
|
|
283
|
+
return this.request(
|
|
284
|
+
"POST",
|
|
285
|
+
`/v1/paid-media/campaigns/${encodeURIComponent(campaignId)}/conversion-goals`,
|
|
286
|
+
input
|
|
287
|
+
);
|
|
288
|
+
}
|
|
278
289
|
announceOrgOnce(method) {
|
|
279
290
|
if (announcedOrg) return;
|
|
280
291
|
if (method.toUpperCase() === "GET") return;
|
|
@@ -699,6 +710,20 @@ var ErdoClient = class {
|
|
|
699
710
|
provisionVoiceCallerName() {
|
|
700
711
|
return this.request("POST", "/v1/voice/caller-name");
|
|
701
712
|
}
|
|
713
|
+
getVoiceBrandedCalling() {
|
|
714
|
+
return this.request("GET", "/v1/voice/branded-calling");
|
|
715
|
+
}
|
|
716
|
+
setVoiceBrandedCalling(body) {
|
|
717
|
+
return this.request("PUT", "/v1/voice/branded-calling", body);
|
|
718
|
+
}
|
|
719
|
+
getVoiceBrandedCallingLOA() {
|
|
720
|
+
return this.request("GET", "/v1/voice/branded-calling/loa");
|
|
721
|
+
}
|
|
722
|
+
// Filing Branded Calling is billed per branded call, so this files the same
|
|
723
|
+
// approval-gated action the chat agent files.
|
|
724
|
+
provisionVoiceBrandedCalling() {
|
|
725
|
+
return this.request("POST", "/v1/voice/branded-calling/provision");
|
|
726
|
+
}
|
|
702
727
|
// Turn an agent's answering of texts on or off. Only the switch changes: the
|
|
703
728
|
// agent's persona, number and calls are untouched, and texts keep arriving and
|
|
704
729
|
// stay readable either way.
|
|
@@ -2692,6 +2717,47 @@ program.command(REFRESH_COMMAND, { hidden: true }).description("Internal: refres
|
|
|
2692
2717
|
await refreshVersionCache();
|
|
2693
2718
|
});
|
|
2694
2719
|
var paidMedia = program.command("paid-media").description("Paid media setup and operations");
|
|
2720
|
+
var paidMediaCampaigns = paidMedia.command("campaigns").description("Google Ads campaigns and their conversion goals");
|
|
2721
|
+
paidMediaCampaigns.command("list").description("List the organization's owned Google Ads campaigns and owned conversion actions").option("--customer-id <id>", "Google Ads customer id (defaults to the selected account)").option("--json", "print the raw JSON result").action(async (opts) => {
|
|
2722
|
+
try {
|
|
2723
|
+
const result = await new ErdoClient().listPaidMediaCampaigns(opts.customerId);
|
|
2724
|
+
if (opts.json) {
|
|
2725
|
+
print(result);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
console.log(`customer id: ${result.customer_id}`);
|
|
2729
|
+
for (const campaign of result.campaigns) {
|
|
2730
|
+
const goal = campaign.goal_config_level === "CAMPAIGN" ? `campaign goal: ${(campaign.conversion_actions ?? []).map((a) => a.name || a.id).join(", ")}` : "customer goals";
|
|
2731
|
+
console.log(`${campaign.id} ${campaign.name} ${campaign.status} ${campaign.bidding_strategy_type || "-"} ${goal}`);
|
|
2732
|
+
}
|
|
2733
|
+
} catch (e) {
|
|
2734
|
+
fail(e);
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
paidMediaCampaigns.command("conversion-goals <campaignId>").description("Set a campaign's custom conversion goal to exactly these conversion action ids").requiredOption("--actions <ids>", "comma-separated conversion action ids").option("--customer-id <id>", "Google Ads customer id (defaults to the selected account)").option("--json", "print the raw JSON result").action(async (campaignId, opts) => {
|
|
2738
|
+
try {
|
|
2739
|
+
const ids = opts.actions.split(",").map((id) => id.trim()).filter(Boolean);
|
|
2740
|
+
if (!ids.length) throw new Error("--actions must contain at least one id");
|
|
2741
|
+
const result = await new ErdoClient().setPaidMediaCampaignConversionGoals(campaignId, {
|
|
2742
|
+
customer_id: opts.customerId,
|
|
2743
|
+
conversion_action_ids: ids
|
|
2744
|
+
});
|
|
2745
|
+
if (opts.json) {
|
|
2746
|
+
print(result);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
console.log(`campaign id: ${result.campaign_id}`);
|
|
2750
|
+
if (result.status === "ready") {
|
|
2751
|
+
console.log("status: ready");
|
|
2752
|
+
if (result.goal_config_level) console.log(`goal config level: ${result.goal_config_level}`);
|
|
2753
|
+
} else {
|
|
2754
|
+
console.log("status: pending approval");
|
|
2755
|
+
console.log(`approval request id: ${result.approval_request_id}`);
|
|
2756
|
+
}
|
|
2757
|
+
} catch (e) {
|
|
2758
|
+
fail(e);
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2695
2761
|
paidMedia.command("conversion-actions").command("ensure").description("Find or approval-gate creation of an offline lead conversion action").requiredOption("--name <name>", "exact Google Ads conversion action name").option("--category <category>", "QUALIFIED_LEAD or SUBMIT_LEAD_FORM", "QUALIFIED_LEAD").option("--customer-id <id>", "Google Ads customer id (defaults to the selected account)").option("--json", "print the raw JSON result").action(async (opts) => {
|
|
2696
2762
|
try {
|
|
2697
2763
|
if (opts.category && opts.category !== "QUALIFIED_LEAD" && opts.category !== "SUBMIT_LEAD_FORM") {
|
|
@@ -6749,7 +6815,7 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
6749
6815
|
} else {
|
|
6750
6816
|
const callable = new Set(res.person_call_numbers ?? []);
|
|
6751
6817
|
printAlignedTable(
|
|
6752
|
-
["number", "agent", "kind", "calls", "status", "waiting on", "reason", "caller name", "shows", "attest."],
|
|
6818
|
+
["number", "agent", "kind", "calls", "status", "waiting on", "reason", "caller name", "shows", "attest.", "brand"],
|
|
6753
6819
|
numbers.map((n) => [
|
|
6754
6820
|
n.address,
|
|
6755
6821
|
n.agent_ref ?? "",
|
|
@@ -6760,7 +6826,8 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
6760
6826
|
n.reason ?? "",
|
|
6761
6827
|
n.caller_name_attachment ?? "",
|
|
6762
6828
|
describeObservedCallerName(n.caller_name_observed, n.observed_caller_name),
|
|
6763
|
-
n.call_attestation ?? ""
|
|
6829
|
+
n.call_attestation ?? "",
|
|
6830
|
+
n.branded_calling_assignment ?? ""
|
|
6764
6831
|
])
|
|
6765
6832
|
);
|
|
6766
6833
|
process.stderr.write(`showing ${numbers.length} number(s)
|
|
@@ -6776,6 +6843,7 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
6776
6843
|
}
|
|
6777
6844
|
printStaleCallerNameNote(numbers);
|
|
6778
6845
|
printCallAttestationNote(res.call_attestation);
|
|
6846
|
+
printBrandedCallingNote(res.branded_calling);
|
|
6779
6847
|
} catch (e) {
|
|
6780
6848
|
fail(e);
|
|
6781
6849
|
}
|
|
@@ -6835,6 +6903,16 @@ function printCallAttestationNote(attestation) {
|
|
|
6835
6903
|
console.log("Complete the business profile with: erdo org business-profile set --help, or in Settings \u2192 Business.");
|
|
6836
6904
|
}
|
|
6837
6905
|
}
|
|
6906
|
+
function printBrandedCallingNote(branded) {
|
|
6907
|
+
if (!branded || branded.status === "approved") return;
|
|
6908
|
+
console.log("");
|
|
6909
|
+
const waiting = branded.next_step ? ` (waiting on ${branded.next_step})` : "";
|
|
6910
|
+
console.log(`Branded calling: ${branded.status}${waiting}.`);
|
|
6911
|
+
if (branded.reason) console.log(branded.reason);
|
|
6912
|
+
if (branded.missing_inputs && branded.missing_inputs.length > 0) {
|
|
6913
|
+
console.log(`Still needed: ${branded.missing_inputs.join(", ")} \u2014 erdo voice branded-calling set --help`);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6838
6916
|
function describeObservedCallerName(observed, name) {
|
|
6839
6917
|
switch (observed) {
|
|
6840
6918
|
case "ours":
|
|
@@ -6955,6 +7033,129 @@ voiceCallerNameCmd.command("provision").description("Register the organization's
|
|
|
6955
7033
|
fail(e);
|
|
6956
7034
|
}
|
|
6957
7035
|
});
|
|
7036
|
+
var voiceBrandedCallingCmd = voiceCmd.command("branded-calling").description("The brand (display name, logo, call reason) mobile phones show when the organization's numbers call");
|
|
7037
|
+
function printBrandedCallingStatus(status) {
|
|
7038
|
+
printAlignedTable(
|
|
7039
|
+
["field", "value"],
|
|
7040
|
+
[
|
|
7041
|
+
["status", status.status],
|
|
7042
|
+
["waiting on", status.next_step ?? ""],
|
|
7043
|
+
["reason", status.reason ?? status.not_registerable_reason ?? ""],
|
|
7044
|
+
["display name", status.inputs.display_name || ""],
|
|
7045
|
+
["legal name", status.legal_name ?? ""],
|
|
7046
|
+
["call reason", status.inputs.call_reason || ""],
|
|
7047
|
+
["logo", status.logo_url ?? "not uploaded"],
|
|
7048
|
+
["signed LOA", status.loa ? status.loa.filename : "not uploaded \u2014 erdo voice branded-calling loa"],
|
|
7049
|
+
["registered as", status.registered_display_name ?? ""],
|
|
7050
|
+
["price", `$${status.price_per_call_usd} per call whose brand is delivered`]
|
|
7051
|
+
]
|
|
7052
|
+
);
|
|
7053
|
+
if (status.numbers.length > 0) {
|
|
7054
|
+
console.log("");
|
|
7055
|
+
printAlignedTable(
|
|
7056
|
+
["number", "agent", "assignment", "note"],
|
|
7057
|
+
status.numbers.map((n) => [
|
|
7058
|
+
n.phone_number,
|
|
7059
|
+
n.agent_slug ?? "",
|
|
7060
|
+
n.assignment,
|
|
7061
|
+
n.not_eligible_reason ?? n.assign_error ?? ""
|
|
7062
|
+
])
|
|
7063
|
+
);
|
|
7064
|
+
}
|
|
7065
|
+
if (status.missing_inputs && status.missing_inputs.length > 0) {
|
|
7066
|
+
console.log("");
|
|
7067
|
+
console.log(`Still needed: ${status.missing_inputs.join(", ")} \u2014 erdo voice branded-calling set --help`);
|
|
7068
|
+
}
|
|
7069
|
+
if (status.missing_profile_fields && status.missing_profile_fields.length > 0) {
|
|
7070
|
+
console.log(`The business profile still needs: ${status.missing_profile_fields.join(", ")}`);
|
|
7071
|
+
}
|
|
7072
|
+
if (status.out_of_date) {
|
|
7073
|
+
console.log("");
|
|
7074
|
+
console.log(
|
|
7075
|
+
"The details changed since they were filed. Register again with: erdo voice branded-calling provision (it replaces the registration; calls show no brand until the new one is approved)."
|
|
7076
|
+
);
|
|
7077
|
+
}
|
|
7078
|
+
}
|
|
7079
|
+
function fileAsBase64(path) {
|
|
7080
|
+
return path ? readFileSync4(path).toString("base64") : void 0;
|
|
7081
|
+
}
|
|
7082
|
+
voiceBrandedCallingCmd.command("get").description("Show the brand, what it still needs, and each number's part in it").option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
|
|
7083
|
+
try {
|
|
7084
|
+
const res = await new ErdoClient().getVoiceBrandedCalling();
|
|
7085
|
+
if (opts.json) {
|
|
7086
|
+
print(res);
|
|
7087
|
+
return;
|
|
7088
|
+
}
|
|
7089
|
+
printBrandedCallingStatus(res);
|
|
7090
|
+
} catch (e) {
|
|
7091
|
+
fail(e);
|
|
7092
|
+
}
|
|
7093
|
+
});
|
|
7094
|
+
voiceBrandedCallingCmd.command("set").description(
|
|
7095
|
+
"Store the brand's details (admins only). Omitted options keep their values. Nothing reaches Twilio until erdo voice branded-calling provision is approved."
|
|
7096
|
+
).option(
|
|
7097
|
+
"--display-name <name>",
|
|
7098
|
+
"shown on the phone, at most 35 characters; defaults to the legal name. Twilio only accepts the legal name or a name legally registered to the business, and rejects the registration otherwise"
|
|
7099
|
+
).option("--call-reason <text>", 'the line under the name, at most 64 characters, e.g. "Your viewing request"').option("--use-case <category>", "CUSTOMER_CARE, ACCOUNT_NOTIFICATION, MARKETING, MIXED, \u2026 (see erdo voice branded-calling get --json)").option("--use-case-description <text>", "what the calls are for").option("--consent-description <text>", "how the people called agreed to be called").option("--daily-calls <n>", "calls expected per day").option("--employees <n>", "number of employees").option("--logo <file>", "the logo: PNG, JPG, WebP, GIF or BMP, at least 128 px; converted to the 256x256 BMP Twilio takes").option("--loa-file <file>", "the signed letter of authorization (PDF, JPG or PNG) \u2014 get it pre-filled with: erdo voice branded-calling loa").option("--json", "print the raw JSON result instead of a table").action(
|
|
7100
|
+
async (opts) => {
|
|
7101
|
+
try {
|
|
7102
|
+
const res = await new ErdoClient().setVoiceBrandedCalling({
|
|
7103
|
+
display_name: opts.displayName,
|
|
7104
|
+
call_reason: opts.callReason,
|
|
7105
|
+
use_case_category: opts.useCase,
|
|
7106
|
+
use_case_description: opts.useCaseDescription,
|
|
7107
|
+
consent_description: opts.consentDescription,
|
|
7108
|
+
daily_call_volume: opts.dailyCalls ? Number(opts.dailyCalls) : void 0,
|
|
7109
|
+
employee_count: opts.employees ? Number(opts.employees) : void 0,
|
|
7110
|
+
logo_base64: fileAsBase64(opts.logo),
|
|
7111
|
+
loa_base64: fileAsBase64(opts.loaFile),
|
|
7112
|
+
loa_filename: opts.loaFile ? basename(opts.loaFile) : void 0
|
|
7113
|
+
});
|
|
7114
|
+
if (opts.json) {
|
|
7115
|
+
print(res);
|
|
7116
|
+
return;
|
|
7117
|
+
}
|
|
7118
|
+
printBrandedCallingStatus(res);
|
|
7119
|
+
} catch (e) {
|
|
7120
|
+
fail(e);
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
);
|
|
7124
|
+
voiceBrandedCallingCmd.command("loa").description("Download the letter of authorization pre-filled from the business profile, for somebody at the business to sign (admins only)").option("--out <file>", "where to write the PDF", "branded-calling-letter-of-authorization.pdf").action(async (opts) => {
|
|
7125
|
+
try {
|
|
7126
|
+
const res = await new ErdoClient().getVoiceBrandedCallingLOA();
|
|
7127
|
+
writeFileSync3(opts.out, Buffer.from(res.content, "base64"));
|
|
7128
|
+
console.log(`Wrote ${opts.out}. Tick the authorization, sign and date it, then upload it with:`);
|
|
7129
|
+
console.log(` erdo voice branded-calling set --loa-file <signed file>`);
|
|
7130
|
+
} catch (e) {
|
|
7131
|
+
fail(e);
|
|
7132
|
+
}
|
|
7133
|
+
});
|
|
7134
|
+
voiceBrandedCallingCmd.command("provision").description("File Branded Calling for every number with A attestation (approval-gated; Twilio charges per branded call)").option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
|
|
7135
|
+
try {
|
|
7136
|
+
const res = await new ErdoClient().provisionVoiceBrandedCalling();
|
|
7137
|
+
if (opts.json) {
|
|
7138
|
+
print(res);
|
|
7139
|
+
return;
|
|
7140
|
+
}
|
|
7141
|
+
if (res.status === "pending_approval") {
|
|
7142
|
+
console.log("Nothing has been registered yet \u2014 an approval card was filed and awaits a decision.");
|
|
7143
|
+
if (res.action_display) console.log(`action: ${res.action_display}`);
|
|
7144
|
+
if (res.approval_request_id) {
|
|
7145
|
+
console.log(`approval request: ${res.approval_request_id}`);
|
|
7146
|
+
console.log(` erdo approvals show ${res.approval_request_id}`);
|
|
7147
|
+
}
|
|
7148
|
+
} else {
|
|
7149
|
+
console.log(`Branded calling ${res.status}.`);
|
|
7150
|
+
if (res.registration !== void 0) print(res.registration);
|
|
7151
|
+
}
|
|
7152
|
+
process.stderr.write(
|
|
7153
|
+
"Twilio reviews it in about 7 business days; re-read it with: erdo voice branded-calling get\n"
|
|
7154
|
+
);
|
|
7155
|
+
} catch (e) {
|
|
7156
|
+
fail(e);
|
|
7157
|
+
}
|
|
7158
|
+
});
|
|
6958
7159
|
var voiceAgentsCmd = voiceCmd.command("agents").description("List the organization's concierges and change settings on one");
|
|
6959
7160
|
voiceAgentsCmd.command("list", { isDefault: true }).description(
|
|
6960
7161
|
"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."
|