@erdoai/cli 0.102.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +163 -10
  2. 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
- console.log(`Still needed before a number can be registered: ${missing.join(", ")}`);
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("--json", "print the raw JSON result instead of a table").action(
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 and their A2P SMS registration state");
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."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.102.0",
3
+ "version": "0.103.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {