@erdoai/cli 0.81.0 → 0.83.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 +339 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5
5
|
import { basename } from "path";
|
|
6
|
-
import { select } from "@inquirer/prompts";
|
|
6
|
+
import { password, select } from "@inquirer/prompts";
|
|
7
7
|
import { Command as Command2 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
@@ -342,13 +342,18 @@ var ErdoClient = class {
|
|
|
342
342
|
);
|
|
343
343
|
}
|
|
344
344
|
// Creates the container — a sub-account under the manager's MCC plus the
|
|
345
|
-
// delegated connection inside the managed org. `
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
345
|
+
// delegated connection inside the managed org. `customerId` switches the call
|
|
346
|
+
// from creating an account to adopting one that already sits directly under
|
|
347
|
+
// the manager, which is the only way to attach a sub-account somebody made by
|
|
348
|
+
// hand: Google Ads cannot delete an account, so creating a second one beside
|
|
349
|
+
// it is permanent. `replaceCurrentAccount` is the one input with consequences:
|
|
350
|
+
// provisioning refuses outright when the org is already running campaigns in
|
|
351
|
+
// another account, and passing this accepts that those campaigns are left
|
|
352
|
+
// behind, so the key is sent only when it is asked for rather than as a
|
|
353
|
+
// `false` the server has to read.
|
|
350
354
|
provisionManagedOrganizationAdsContainer(slug, input) {
|
|
351
355
|
const body = {};
|
|
356
|
+
if (input.customerId) body.customer_id = input.customerId;
|
|
352
357
|
if (input.descriptiveName) body.descriptive_name = input.descriptiveName;
|
|
353
358
|
if (input.currencyCode) body.currency_code = input.currencyCode;
|
|
354
359
|
if (input.timeZone) body.time_zone = input.timeZone;
|
|
@@ -534,6 +539,39 @@ var ErdoClient = class {
|
|
|
534
539
|
`/v1/voice/sms-conversations/${encodeURIComponent(sessionID)}${qs ? `?${qs}` : ""}`
|
|
535
540
|
);
|
|
536
541
|
}
|
|
542
|
+
// The organization's business profile: its legal identity, which every A2P
|
|
543
|
+
// registration on the account is checked against. It follows the org
|
|
544
|
+
// convention rather than the voice one because it is the organization's, not
|
|
545
|
+
// any one number's.
|
|
546
|
+
getBusinessProfile() {
|
|
547
|
+
return this.request("GET", "/v1/org/business-profile");
|
|
548
|
+
}
|
|
549
|
+
// PUT rather than POST: there is exactly one profile per organization and a
|
|
550
|
+
// save replaces it, so a caller sending a partial body blanks what it omits.
|
|
551
|
+
setBusinessProfile(body) {
|
|
552
|
+
return this.request("PUT", "/v1/org/business-profile", body);
|
|
553
|
+
}
|
|
554
|
+
// The numbers the organization holds, with what each one's SMS registration
|
|
555
|
+
// is waiting on. Nothing here reaches the provider — the state is read from
|
|
556
|
+
// the row the registration action and the status cron maintain.
|
|
557
|
+
listVoicePhoneNumbers() {
|
|
558
|
+
return this.request("GET", "/v1/voice/phone-numbers");
|
|
559
|
+
}
|
|
560
|
+
getVoicePhoneNumberSMS(number) {
|
|
561
|
+
return this.request(
|
|
562
|
+
"GET",
|
|
563
|
+
`/v1/voice/phone-numbers/${encodeURIComponent(number)}/sms`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
// Asking for a registration costs money at the carrier registry, so this
|
|
567
|
+
// files the same approval-gated action the chat agent files rather than
|
|
568
|
+
// registering anything itself.
|
|
569
|
+
provisionVoicePhoneNumberSMS(number) {
|
|
570
|
+
return this.request(
|
|
571
|
+
"POST",
|
|
572
|
+
`/v1/voice/phone-numbers/${encodeURIComponent(number)}/sms`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
537
575
|
// Run a read-only HogQL query against the org's page-analytics events. Rows are
|
|
538
576
|
// positional per columns; enabled:false means page analytics is off for the org
|
|
539
577
|
// (not zero traffic). A rejected query surfaces PostHog's message as the error.
|
|
@@ -1635,6 +1673,25 @@ function readMaybeFile(value) {
|
|
|
1635
1673
|
if (value.startsWith("@@")) return value.slice(1);
|
|
1636
1674
|
return value.startsWith("@") ? readFileSync2(value.slice(1), "utf8") : value;
|
|
1637
1675
|
}
|
|
1676
|
+
async function resolveSecretInput(options) {
|
|
1677
|
+
if (options.fromStdin) {
|
|
1678
|
+
const piped = (await options.readStdin()).trim();
|
|
1679
|
+
if (!piped) {
|
|
1680
|
+
throw new Error("--tax-id-stdin was given but standard input was empty");
|
|
1681
|
+
}
|
|
1682
|
+
return piped;
|
|
1683
|
+
}
|
|
1684
|
+
const wanted = options.replace || !options.stored;
|
|
1685
|
+
if (wanted && options.interactive) {
|
|
1686
|
+
return (await options.prompt(options.stored)).trim() || void 0;
|
|
1687
|
+
}
|
|
1688
|
+
if (options.replace) {
|
|
1689
|
+
throw new Error(
|
|
1690
|
+
"--replace-tax-id needs a terminal to prompt on. In a script, pipe the value in instead: `... | erdo org business-profile set --tax-id-stdin`"
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
return void 0;
|
|
1694
|
+
}
|
|
1638
1695
|
|
|
1639
1696
|
// src/scope.ts
|
|
1640
1697
|
import { Option } from "commander";
|
|
@@ -1800,6 +1857,16 @@ function timedOutMessage(threadID) {
|
|
|
1800
1857
|
function print(value) {
|
|
1801
1858
|
console.log(JSON.stringify(value, null, 2));
|
|
1802
1859
|
}
|
|
1860
|
+
async function readAllStdin() {
|
|
1861
|
+
if (process.stdin.isTTY) {
|
|
1862
|
+
throw new Error("nothing is piped to standard input");
|
|
1863
|
+
}
|
|
1864
|
+
const chunks = [];
|
|
1865
|
+
for await (const chunk of process.stdin) {
|
|
1866
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1867
|
+
}
|
|
1868
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1869
|
+
}
|
|
1803
1870
|
function readJSONObject(file, label) {
|
|
1804
1871
|
let parsed;
|
|
1805
1872
|
try {
|
|
@@ -2057,6 +2124,139 @@ org.command("autonomy [mode]").description("Show or set the org's engine-autonom
|
|
|
2057
2124
|
fail(e);
|
|
2058
2125
|
}
|
|
2059
2126
|
});
|
|
2127
|
+
var businessProfileCmd = org.command("business-profile").description("The org's legal identity, which A2P SMS registration is checked against");
|
|
2128
|
+
function printBusinessProfile(profile) {
|
|
2129
|
+
printAlignedTable(
|
|
2130
|
+
["field", "value"],
|
|
2131
|
+
[
|
|
2132
|
+
["legal name", profile.legal_name],
|
|
2133
|
+
["entity type", profile.entity_type],
|
|
2134
|
+
["legal structure", profile.legal_structure],
|
|
2135
|
+
// Only shown for the one entity type that has them, so a private
|
|
2136
|
+
// company's table does not carry two rows reading "—".
|
|
2137
|
+
...profile.stock_exchange || profile.stock_ticker ? [
|
|
2138
|
+
["stock exchange", profile.stock_exchange],
|
|
2139
|
+
["stock ticker", profile.stock_ticker]
|
|
2140
|
+
] : [],
|
|
2141
|
+
["tax id", profile.tax_id_stored ? `stored, ending ${profile.tax_id_last4 || "????"}` : "none stored"],
|
|
2142
|
+
["street", profile.street],
|
|
2143
|
+
["city", profile.city],
|
|
2144
|
+
["region", profile.region],
|
|
2145
|
+
["postal code", profile.postal_code],
|
|
2146
|
+
["country", profile.country],
|
|
2147
|
+
["website", profile.website_url],
|
|
2148
|
+
["industry", profile.industry],
|
|
2149
|
+
["privacy policy", profile.privacy_policy_url],
|
|
2150
|
+
["terms", profile.terms_and_conditions_url],
|
|
2151
|
+
["representative", `${profile.representative_first_name} ${profile.representative_last_name}`.trim()],
|
|
2152
|
+
["title", profile.representative_title],
|
|
2153
|
+
["job position", profile.representative_job_position],
|
|
2154
|
+
["email", profile.representative_email],
|
|
2155
|
+
["phone", profile.representative_phone],
|
|
2156
|
+
["updated", profile.updated_at ?? ""]
|
|
2157
|
+
]
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
function printMissingProfileFields(missing) {
|
|
2161
|
+
if (missing.length === 0) return;
|
|
2162
|
+
console.log("");
|
|
2163
|
+
console.log(`Still needed before a number can be registered: ${missing.join(", ")}`);
|
|
2164
|
+
console.log("Fill them in with: erdo org business-profile set --help");
|
|
2165
|
+
}
|
|
2166
|
+
businessProfileCmd.command("get").description("Show the stored business profile and what SMS registration still needs").option(
|
|
2167
|
+
"--json",
|
|
2168
|
+
"print the raw JSON result, which also carries the accepted entity types, legal structures, industries and job positions"
|
|
2169
|
+
).action(async (opts) => {
|
|
2170
|
+
try {
|
|
2171
|
+
const res = await new ErdoClient().getBusinessProfile();
|
|
2172
|
+
if (opts.json) {
|
|
2173
|
+
print(res);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
if (!res.exists) {
|
|
2177
|
+
console.log("No business profile saved for this organization.");
|
|
2178
|
+
if (res.missing_fields.length > 0) {
|
|
2179
|
+
console.log(`Registration needs: ${res.missing_fields.join(", ")}`);
|
|
2180
|
+
}
|
|
2181
|
+
console.log("Save one with: erdo org business-profile set --help");
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
printBusinessProfile(res.profile);
|
|
2185
|
+
printMissingProfileFields(res.missing_fields);
|
|
2186
|
+
} catch (e) {
|
|
2187
|
+
fail(e);
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
businessProfileCmd.command("set").description("Save the organization's business profile (unset flags keep what is stored)").option("--legal-name <name>", "registered legal name, exactly as it appears on the tax record").option(
|
|
2191
|
+
"--entity-type <type>",
|
|
2192
|
+
"private_for_profit, public_for_profit, non_profit, sole_proprietor or government"
|
|
2193
|
+
).option(
|
|
2194
|
+
"--legal-structure <value>",
|
|
2195
|
+
"the legal form the carrier registry checks against the tax record: sole_proprietorship, partnership, limited_liability_corporation, co_operative, non_profit_corporation or corporation"
|
|
2196
|
+
).option("--stock-exchange <exchange>", "a public company's exchange, e.g. NYSE \u2014 required for public_for_profit").option("--stock-ticker <ticker>", "a public company's ticker, e.g. ACME \u2014 required for public_for_profit").option(
|
|
2197
|
+
"--replace-tax-id",
|
|
2198
|
+
"replace the stored tax id: prompts for the new EIN without echoing it. Omit to keep what is stored"
|
|
2199
|
+
).option(
|
|
2200
|
+
"--tax-id-stdin",
|
|
2201
|
+
"read the tax id from standard input instead of prompting, for a script \u2014 e.g. `pass show ein | erdo org business-profile set --tax-id-stdin`"
|
|
2202
|
+
).option("--street <street>", "street address of the registered business").option("--city <city>", "city of the registered business").option("--region <region>", "state or province, e.g. FL").option("--postal-code <code>", "postal or ZIP code").option("--country <iso2>", "two-letter ISO country code, e.g. US").option("--website-url <url>", "the business's own website \u2014 carriers check that it describes it").option("--industry <industry>", "one of the industries listed by: erdo org business-profile get --json").option("--privacy-policy-url <url>", "the business's own privacy policy page (https) \u2014 a campaign without one is rejected").option("--terms-url <url>", "the business's own terms and conditions page (https) \u2014 required for the same reason").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(
|
|
2203
|
+
"--job-position <position>",
|
|
2204
|
+
"their role from the fixed list (ceo, cfo, director, general_counsel, gm, vp, other)"
|
|
2205
|
+
).option("--email <email>", "their email address").option("--phone <number>", "their phone number in E.164, e.g. +13055550123").option("--json", "print the raw JSON result instead of a table").action(
|
|
2206
|
+
async (opts) => {
|
|
2207
|
+
try {
|
|
2208
|
+
const api = new ErdoClient();
|
|
2209
|
+
const current = await api.getBusinessProfile();
|
|
2210
|
+
const stored = current.profile;
|
|
2211
|
+
const taxID = await resolveSecretInput({
|
|
2212
|
+
replace: opts.replaceTaxId === true,
|
|
2213
|
+
stored: stored.tax_id_stored,
|
|
2214
|
+
fromStdin: opts.taxIdStdin === true,
|
|
2215
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
2216
|
+
readStdin: readAllStdin,
|
|
2217
|
+
prompt: (replacing) => password({
|
|
2218
|
+
message: replacing ? "New business tax id (EIN \u2014 digits; it replaces the stored one):" : "Business tax id (EIN \u2014 digits; leave blank if the business has none):",
|
|
2219
|
+
mask: "*"
|
|
2220
|
+
})
|
|
2221
|
+
});
|
|
2222
|
+
const entityType = opts.entityType ?? stored.entity_type;
|
|
2223
|
+
const isPublic = entityType === "public_for_profit";
|
|
2224
|
+
const body = {
|
|
2225
|
+
legal_name: opts.legalName ?? stored.legal_name,
|
|
2226
|
+
entity_type: entityType,
|
|
2227
|
+
legal_structure: opts.legalStructure ?? stored.legal_structure,
|
|
2228
|
+
stock_exchange: isPublic ? opts.stockExchange ?? stored.stock_exchange : void 0,
|
|
2229
|
+
stock_ticker: isPublic ? opts.stockTicker ?? stored.stock_ticker : void 0,
|
|
2230
|
+
tax_id: taxID,
|
|
2231
|
+
street: opts.street ?? stored.street,
|
|
2232
|
+
city: opts.city ?? stored.city,
|
|
2233
|
+
region: opts.region ?? stored.region,
|
|
2234
|
+
postal_code: opts.postalCode ?? stored.postal_code,
|
|
2235
|
+
country: opts.country ?? stored.country,
|
|
2236
|
+
website_url: opts.websiteUrl ?? stored.website_url,
|
|
2237
|
+
industry: opts.industry ?? stored.industry,
|
|
2238
|
+
privacy_policy_url: opts.privacyPolicyUrl ?? stored.privacy_policy_url,
|
|
2239
|
+
terms_and_conditions_url: opts.termsUrl ?? stored.terms_and_conditions_url,
|
|
2240
|
+
representative_first_name: opts.firstName ?? stored.representative_first_name,
|
|
2241
|
+
representative_last_name: opts.lastName ?? stored.representative_last_name,
|
|
2242
|
+
representative_title: opts.title ?? stored.representative_title,
|
|
2243
|
+
representative_job_position: opts.jobPosition ?? stored.representative_job_position,
|
|
2244
|
+
representative_email: opts.email ?? stored.representative_email,
|
|
2245
|
+
representative_phone: opts.phone ?? stored.representative_phone
|
|
2246
|
+
};
|
|
2247
|
+
const res = await api.setBusinessProfile(body);
|
|
2248
|
+
if (opts.json) {
|
|
2249
|
+
print(res);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
console.log("Business profile saved.");
|
|
2253
|
+
printBusinessProfile(res.profile);
|
|
2254
|
+
printMissingProfileFields(res.missing_fields);
|
|
2255
|
+
} catch (e) {
|
|
2256
|
+
fail(e);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
);
|
|
2060
2260
|
var managed = org.command("managed").description("Operate client orgs via a manager account");
|
|
2061
2261
|
managed.command("list").description("List the client orgs your org manages").action(async () => {
|
|
2062
2262
|
try {
|
|
@@ -2153,6 +2353,9 @@ function printAdsContainer(c) {
|
|
|
2153
2353
|
managed.command("ads-container <orgSlug>").description(
|
|
2154
2354
|
"Read the Google Ads account a managed org runs its campaigns in, or --provision one under your manager account"
|
|
2155
2355
|
).option("--provision", "create the account and the org's delegated google_ads connection instead of reading them").option(
|
|
2356
|
+
"--adopt <customerId>",
|
|
2357
|
+
"with --provision: connect an existing client account already sitting directly under your manager account instead of creating a new one \u2014 for a sub-account created by hand in the Google Ads UI"
|
|
2358
|
+
).option(
|
|
2156
2359
|
"--replace-current-account",
|
|
2157
2360
|
"with --provision: go ahead even though the org already runs campaigns in another account. Google Ads cannot move a campaign, so those campaigns stay behind and have to be rebuilt"
|
|
2158
2361
|
).option("--name <descriptiveName>", "display name for the new account (default: the org's name and slug)").option("--currency <code>", "ISO currency code for the new account, e.g. USD").option("--time-zone <tz>", "IANA time zone for the new account, e.g. America/New_York").action(
|
|
@@ -2161,6 +2364,16 @@ managed.command("ads-container <orgSlug>").description(
|
|
|
2161
2364
|
if (opts.replaceCurrentAccount && !opts.provision) {
|
|
2162
2365
|
fail(new Error("--replace-current-account only applies with --provision"));
|
|
2163
2366
|
}
|
|
2367
|
+
if (opts.adopt && !opts.provision) {
|
|
2368
|
+
fail(new Error("--adopt only applies with --provision"));
|
|
2369
|
+
}
|
|
2370
|
+
if (opts.adopt && (opts.name || opts.currency || opts.timeZone)) {
|
|
2371
|
+
fail(
|
|
2372
|
+
new Error(
|
|
2373
|
+
"--adopt takes an account that already exists, so --name, --currency and --time-zone do not apply \u2014 they describe an account being created"
|
|
2374
|
+
)
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2164
2377
|
const api = new ErdoClient();
|
|
2165
2378
|
if (!opts.provision) {
|
|
2166
2379
|
try {
|
|
@@ -2178,13 +2391,14 @@ managed.command("ads-container <orgSlug>").description(
|
|
|
2178
2391
|
return;
|
|
2179
2392
|
}
|
|
2180
2393
|
const c = await api.provisionManagedOrganizationAdsContainer(orgSlug, {
|
|
2394
|
+
customerId: opts.adopt,
|
|
2181
2395
|
descriptiveName: opts.name,
|
|
2182
2396
|
currencyCode: opts.currency,
|
|
2183
2397
|
timeZone: opts.timeZone,
|
|
2184
2398
|
replaceCurrentAccount: opts.replaceCurrentAccount
|
|
2185
2399
|
});
|
|
2186
2400
|
console.log(
|
|
2187
|
-
c.already_provisioned ? `Google Ads container already existed for ${c.org_slug}` : `Created Google Ads container for ${c.org_slug}`
|
|
2401
|
+
c.already_provisioned ? `Google Ads container already existed for ${c.org_slug}` : c.adopted ? `Adopted Google Ads account ${c.customer_id} as the container for ${c.org_slug}` : `Created Google Ads container for ${c.org_slug}`
|
|
2188
2402
|
);
|
|
2189
2403
|
printAdsContainer(c);
|
|
2190
2404
|
const leftBehind = c.previous_campaign_count ?? 0;
|
|
@@ -4872,7 +5086,7 @@ sentEmailsCmd.command("get <emailID>").description("Read one sent email, includi
|
|
|
4872
5086
|
fail(e);
|
|
4873
5087
|
}
|
|
4874
5088
|
});
|
|
4875
|
-
var voiceCmd = program.command("voice").description("
|
|
5089
|
+
var voiceCmd = program.command("voice").description("A voice agent's phone calls, text conversations, website widget conversations, and its numbers");
|
|
4876
5090
|
function contactLabel(contact) {
|
|
4877
5091
|
if (!contact) return "-";
|
|
4878
5092
|
const name = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
|
|
@@ -5115,6 +5329,123 @@ voiceSMSCmd.command("get <sessionID>").description("Read one SMS conversation: i
|
|
|
5115
5329
|
}
|
|
5116
5330
|
}
|
|
5117
5331
|
);
|
|
5332
|
+
var voiceNumbersCmd = voiceCmd.command("numbers").description("The organization's phone numbers and their A2P SMS registration state");
|
|
5333
|
+
function printRegistrationState(registration) {
|
|
5334
|
+
if (registration === null || typeof registration !== "object") {
|
|
5335
|
+
if (registration !== void 0) print(registration);
|
|
5336
|
+
return;
|
|
5337
|
+
}
|
|
5338
|
+
const state = registration;
|
|
5339
|
+
printAlignedTable(
|
|
5340
|
+
["number", "kind", "status", "waiting on", "reason"],
|
|
5341
|
+
[
|
|
5342
|
+
[
|
|
5343
|
+
state.address ?? "",
|
|
5344
|
+
state.sender_kind ?? "",
|
|
5345
|
+
state.status ?? "",
|
|
5346
|
+
state.next_step ?? "",
|
|
5347
|
+
state.reason ?? ""
|
|
5348
|
+
]
|
|
5349
|
+
]
|
|
5350
|
+
);
|
|
5351
|
+
printProfileDriftNote([state]);
|
|
5352
|
+
}
|
|
5353
|
+
function printProfileDriftNote(numbers) {
|
|
5354
|
+
const drifted = numbers.filter((n) => n.profile_out_of_date);
|
|
5355
|
+
if (drifted.length === 0) return;
|
|
5356
|
+
console.log("");
|
|
5357
|
+
for (const number of drifted) {
|
|
5358
|
+
const fields = number.profile_out_of_date_fields ?? [];
|
|
5359
|
+
console.log(
|
|
5360
|
+
`${number.address ?? "this number"}: the business profile has changed since it was registered${fields.length > 0 ? ` (${fields.join(", ")})` : ""} \u2014 the carriers still hold the previous legal identity.`
|
|
5361
|
+
);
|
|
5362
|
+
}
|
|
5363
|
+
console.log(
|
|
5364
|
+
"Re-send it with: erdo voice numbers provision-sms <number>. It re-opens carrier review, which takes days."
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
voiceNumbersCmd.command("list").description("List the organization's numbers with what each one's SMS registration is waiting on").option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
|
|
5368
|
+
try {
|
|
5369
|
+
const res = await new ErdoClient().listVoicePhoneNumbers();
|
|
5370
|
+
if (opts.json) {
|
|
5371
|
+
print(res);
|
|
5372
|
+
return;
|
|
5373
|
+
}
|
|
5374
|
+
const numbers = res.numbers ?? [];
|
|
5375
|
+
if (numbers.length === 0) {
|
|
5376
|
+
console.log("This organization holds no phone numbers.");
|
|
5377
|
+
} else {
|
|
5378
|
+
printAlignedTable(
|
|
5379
|
+
["number", "agent", "kind", "status", "waiting on", "reason"],
|
|
5380
|
+
numbers.map((n) => [
|
|
5381
|
+
n.address,
|
|
5382
|
+
n.agent_ref ?? "",
|
|
5383
|
+
n.sender_kind,
|
|
5384
|
+
n.status,
|
|
5385
|
+
n.next_step ?? "",
|
|
5386
|
+
n.reason ?? ""
|
|
5387
|
+
])
|
|
5388
|
+
);
|
|
5389
|
+
process.stderr.write(`showing ${numbers.length} number(s)
|
|
5390
|
+
`);
|
|
5391
|
+
printProfileDriftNote(numbers);
|
|
5392
|
+
}
|
|
5393
|
+
if (!res.business_profile_complete) {
|
|
5394
|
+
console.log("");
|
|
5395
|
+
console.log(
|
|
5396
|
+
res.missing_profile_fields.length > 0 ? `The business profile is incomplete \u2014 registration needs: ${res.missing_profile_fields.join(", ")}` : "The business profile is incomplete \u2014 no profile has been saved yet."
|
|
5397
|
+
);
|
|
5398
|
+
console.log("Complete it with: erdo org business-profile set --help, or in Settings \u2192 Business.");
|
|
5399
|
+
}
|
|
5400
|
+
} catch (e) {
|
|
5401
|
+
fail(e);
|
|
5402
|
+
}
|
|
5403
|
+
});
|
|
5404
|
+
voiceNumbersCmd.command("get <number>").description("Read one number's SMS registration state and what it is waiting on").option("--json", "print the raw JSON result instead of a summary").action(async (number, opts) => {
|
|
5405
|
+
try {
|
|
5406
|
+
const res = await new ErdoClient().getVoicePhoneNumberSMS(number);
|
|
5407
|
+
if (opts.json) {
|
|
5408
|
+
print(res);
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
printRegistrationState(res);
|
|
5412
|
+
if (res.missing_profile_fields && res.missing_profile_fields.length > 0) {
|
|
5413
|
+
console.log("");
|
|
5414
|
+
console.log(
|
|
5415
|
+
`The business profile is incomplete \u2014 registration needs: ${res.missing_profile_fields.join(", ")}`
|
|
5416
|
+
);
|
|
5417
|
+
console.log("Complete it with: erdo org business-profile set --help, or in Settings \u2192 Business.");
|
|
5418
|
+
}
|
|
5419
|
+
} catch (e) {
|
|
5420
|
+
fail(e);
|
|
5421
|
+
}
|
|
5422
|
+
});
|
|
5423
|
+
voiceNumbersCmd.command("provision-sms <number>").description("Ask for A2P SMS registration of one of the organization's numbers").option("--json", "print the raw JSON result instead of a summary").action(async (number, opts) => {
|
|
5424
|
+
try {
|
|
5425
|
+
const res = await new ErdoClient().provisionVoicePhoneNumberSMS(number);
|
|
5426
|
+
if (opts.json) {
|
|
5427
|
+
print(res);
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5430
|
+
if (res.status === "pending_approval") {
|
|
5431
|
+
console.log("Nothing has been registered yet \u2014 an approval card was filed and awaits a decision.");
|
|
5432
|
+
if (res.action_display) console.log(`action: ${res.action_display}`);
|
|
5433
|
+
if (res.approval_request_id) {
|
|
5434
|
+
console.log(`approval request: ${res.approval_request_id}`);
|
|
5435
|
+
console.log(` erdo approvals show ${res.approval_request_id}`);
|
|
5436
|
+
console.log(` erdo approvals decide ${res.approval_request_id} --approve`);
|
|
5437
|
+
}
|
|
5438
|
+
} else {
|
|
5439
|
+
console.log(`Registration ${res.status}.`);
|
|
5440
|
+
printRegistrationState(res.registration);
|
|
5441
|
+
}
|
|
5442
|
+
process.stderr.write(
|
|
5443
|
+
"Carrier review takes days once submitted; the status updates on its own \u2014 re-read it with: erdo voice numbers list\n"
|
|
5444
|
+
);
|
|
5445
|
+
} catch (e) {
|
|
5446
|
+
fail(e);
|
|
5447
|
+
}
|
|
5448
|
+
});
|
|
5118
5449
|
var datasetsCmd = program.command("datasets").description("Datasets");
|
|
5119
5450
|
datasetsCmd.command("list").description("List datasets").option(
|
|
5120
5451
|
"--class <class>",
|