@erdoai/cli 0.105.0 → 0.108.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 +176 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -576,6 +576,31 @@ var ErdoClient = class {
576
576
  `/v1/voice/sms-conversations/${encodeURIComponent(sessionID)}${qs ? `?${qs}` : ""}`
577
577
  );
578
578
  }
579
+ // WhatsApp conversations on those same numbers — their own resource rather
580
+ // than a channel on the SMS reads, addressed and filtered the same way.
581
+ listWhatsAppConversations(params) {
582
+ const q = new URLSearchParams();
583
+ if (params?.phone_number) q.set("phone_number", params.phone_number);
584
+ if (params?.address) q.set("address", params.address);
585
+ if (params?.canonical_lead_id) q.set("canonical_lead_id", params.canonical_lead_id);
586
+ if (params?.limit !== void 0) q.set("limit", String(params.limit));
587
+ if (params?.cursor) q.set("cursor", params.cursor);
588
+ const qs = q.toString();
589
+ return this.request(
590
+ "GET",
591
+ `/v1/voice/whatsapp-conversations${qs ? `?${qs}` : ""}`
592
+ );
593
+ }
594
+ getWhatsAppConversation(sessionID, params) {
595
+ const q = new URLSearchParams();
596
+ if (params?.limit !== void 0) q.set("limit", String(params.limit));
597
+ if (params?.cursor) q.set("cursor", params.cursor);
598
+ const qs = q.toString();
599
+ return this.request(
600
+ "GET",
601
+ `/v1/voice/whatsapp-conversations/${encodeURIComponent(sessionID)}${qs ? `?${qs}` : ""}`
602
+ );
603
+ }
579
604
  // The organization's business profile: its legal identity, which every A2P
580
605
  // registration on the account is checked against. It follows the org
581
606
  // convention rather than the voice one because it is the organization's, not
@@ -5968,6 +5993,127 @@ contact: ${parts.join(" \xB7 ")}`);
5968
5993
  }
5969
5994
  }
5970
5995
  );
5996
+ var voiceWhatsAppCmd = voiceCmd.command("whatsapp").description("WhatsApp conversations on a voice agent's own number");
5997
+ voiceWhatsAppCmd.command("list").description("List the organization's WhatsApp conversations, most recently active first").option("--phone-number <number>", "only conversations on this Erdo number (any format)").option("--address <number>", "only the conversation with this person (any format)").option("--lead <lead>", LEAD_FILTER_HELP).option("--limit <n>", "page size (default 25, max 100)").option("--cursor <cursor>", "next_cursor from the preceding page (stable paging)").option("--json", "print the raw JSON result instead of a table").action(
5998
+ async (opts) => {
5999
+ try {
6000
+ const res = await new ErdoClient().listWhatsAppConversations({
6001
+ phone_number: opts.phoneNumber,
6002
+ address: opts.address,
6003
+ canonical_lead_id: opts.lead,
6004
+ limit: opts.limit ? Number(opts.limit) : void 0,
6005
+ cursor: opts.cursor
6006
+ });
6007
+ if (opts.json) {
6008
+ print(res);
6009
+ return;
6010
+ }
6011
+ const conversations = res.conversations ?? [];
6012
+ if (conversations.length === 0) {
6013
+ console.log("No WhatsApp conversations match those filters.");
6014
+ return;
6015
+ }
6016
+ printAlignedTable(
6017
+ ["session id", "phone", "address", "name", "msgs", "last inbound", "last outbound", "lead ref", "summary"],
6018
+ conversations.map((c) => [
6019
+ c.session_id,
6020
+ c.phone_number,
6021
+ c.suppressed ? `${c.address} (suppressed)` : c.address,
6022
+ c.display_name ?? "",
6023
+ c.message_count,
6024
+ c.last_inbound_at ?? "",
6025
+ c.last_outbound_at ?? "",
6026
+ leadReferenceLabel(c.lead_reference),
6027
+ c.summary
6028
+ ])
6029
+ );
6030
+ process.stderr.write(`showing ${conversations.length} conversation(s)
6031
+ `);
6032
+ if (res.next_cursor) {
6033
+ process.stderr.write(`more available \u2014 re-run with --cursor ${res.next_cursor}
6034
+ `);
6035
+ }
6036
+ } catch (e) {
6037
+ fail(e);
6038
+ }
6039
+ }
6040
+ );
6041
+ function whatsAppMessageText(m) {
6042
+ const parts = [];
6043
+ if (m.body) parts.push(m.body);
6044
+ if (m.location) {
6045
+ const where = [m.location.label, m.location.address].filter(Boolean).join(", ");
6046
+ parts.push(`[location ${m.location.latitude},${m.location.longitude}${where ? ` ${where}` : ""}]`);
6047
+ }
6048
+ if (m.context?.button_text) parts.push(`[tapped "${m.context.button_text}"]`);
6049
+ else if (m.context?.replied_message_id) parts.push(`[reply to ${m.context.replied_message_id}]`);
6050
+ if (m.referral) parts.push(`[from ad${m.referral.Headline ? ` "${m.referral.Headline}"` : ""}]`);
6051
+ if (parts.length === 0 && m.message_type && m.message_type !== "text") parts.push(`[${m.message_type}]`);
6052
+ return parts.join(" ");
6053
+ }
6054
+ voiceWhatsAppCmd.command("get <sessionID>").description("Read one WhatsApp conversation: its metadata, then every message oldest first").option("--limit <n>", "messages per page (default 50, max 200)").option("--cursor <cursor>", "next_cursor from the preceding page of messages").option("--json", "print the raw JSON result instead of a table").action(
6055
+ async (sessionID, opts) => {
6056
+ try {
6057
+ const res = await new ErdoClient().getWhatsAppConversation(sessionID, {
6058
+ limit: opts.limit ? Number(opts.limit) : void 0,
6059
+ cursor: opts.cursor
6060
+ });
6061
+ if (opts.json) {
6062
+ print(res);
6063
+ return;
6064
+ }
6065
+ const c = res.conversation;
6066
+ printAlignedTable(
6067
+ ["session id", "phone", "address", "name", "msgs", "last inbound", "last outbound", "summary"],
6068
+ [
6069
+ [
6070
+ c.session_id,
6071
+ c.phone_number,
6072
+ c.suppressed ? `${c.address} (suppressed)` : c.address,
6073
+ c.display_name ?? "",
6074
+ c.message_count,
6075
+ c.last_inbound_at ?? "",
6076
+ c.last_outbound_at ?? "",
6077
+ c.summary
6078
+ ]
6079
+ ]
6080
+ );
6081
+ const contact = c.contact;
6082
+ if (contact && (contact.name || contact.email || contact.phone || contact.company)) {
6083
+ const parts = [contact.name, contact.email, contact.phone, contact.company].filter(Boolean);
6084
+ console.log(`
6085
+ contact: ${parts.join(" \xB7 ")}`);
6086
+ }
6087
+ if (c.lead_reference) {
6088
+ console.log(`lead: ${c.lead_reference} (${c.canonical_lead_id})`);
6089
+ }
6090
+ const messages = res.messages ?? [];
6091
+ if (messages.length === 0) {
6092
+ console.log("\nNo messages have been recorded in this conversation yet.");
6093
+ return;
6094
+ }
6095
+ console.log("");
6096
+ printAlignedTable(
6097
+ ["at", "direction", "status", "media", "message"],
6098
+ messages.map((m) => [
6099
+ m.created_at,
6100
+ m.direction,
6101
+ m.status,
6102
+ m.num_media > 0 ? m.num_media : "",
6103
+ whatsAppMessageText(m)
6104
+ ])
6105
+ );
6106
+ process.stderr.write(`showing ${messages.length} message(s), oldest first
6107
+ `);
6108
+ if (res.next_cursor) {
6109
+ process.stderr.write(`more available \u2014 re-run with --cursor ${res.next_cursor}
6110
+ `);
6111
+ }
6112
+ } catch (e) {
6113
+ fail(e);
6114
+ }
6115
+ }
6116
+ );
5971
6117
  var voiceNumbersCmd = voiceCmd.command("numbers").description("The organization's phone numbers, their A2P SMS registration and their caller name");
5972
6118
  function printRegistrationState(registration) {
5973
6119
  if (registration === null || typeof registration !== "object") {
@@ -6913,7 +7059,7 @@ var leadsCmd = program.command("leads").description(
6913
7059
  "Read leads in a lead dataset, merge two that are the same person, and run the lead playbook that suggests each lead's next action"
6914
7060
  );
6915
7061
  leadsCmd.command("get <dataset> <lead>").description(
6916
- "Read one lead: its permanent canonical_lead_id, the email and phone evidence bound to it, its dataset row, the leads merged into it, and its capture history. <lead> is either the canonical_lead_id UUID or the 22-character lead reference, and an id that has since been merged away resolves to the lead it became \u2014 so an id read off an old capture response still works."
7062
+ "Read one lead: its permanent canonical_lead_id, what it told us (on the form and in every call, chat, text and email reply since), the email and phone evidence bound to it, its dataset row, the leads merged into it, and its capture history. <lead> is either the canonical_lead_id UUID or the 22-character lead reference, and an id that has since been merged away resolves to the lead it became \u2014 so an id read off an old capture response still works."
6917
7063
  ).option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
6918
7064
  try {
6919
7065
  const res = await new ErdoClient().getLead(dataset, lead);
@@ -6999,7 +7145,7 @@ leadsCmd.command("merge <dataset> <survivor>").description(
6999
7145
  console.log(`Already one lead: ${res.absorbed_lead_id} resolves to ${res.lead.canonical_lead_id}.`);
7000
7146
  } else {
7001
7147
  console.log(
7002
- `Merged ${res.absorbed_lead_id} into ${res.lead.canonical_lead_id} \u2014 ${res.aliases_moved} contact ${res.aliases_moved === 1 ? "alias" : "aliases"} moved, ${res.rows_combined} ${res.rows_combined === 1 ? "row" : "rows"} combined.`
7148
+ `Merged ${res.absorbed_lead_id} into ${res.lead.canonical_lead_id} \u2014 ${res.aliases_moved} contact ${res.aliases_moved === 1 ? "alias" : "aliases"} and ${res.details_moved ?? 0} ${res.details_moved === 1 ? "detail" : "details"} moved, ${res.rows_combined} ${res.rows_combined === 1 ? "row" : "rows"} combined.`
7003
7149
  );
7004
7150
  }
7005
7151
  printLead(res.lead);
@@ -7406,6 +7552,24 @@ function printRecordedLeadDecision(a) {
7406
7552
  console.log(`Recorded ${a.action_kind} on lead ${a.lead_reference}${a.action_kind === "hold" ? until : ""}.`);
7407
7553
  if (a.rationale) console.log(a.rationale);
7408
7554
  }
7555
+ function leadDetailSource(kind) {
7556
+ switch (kind) {
7557
+ case "form":
7558
+ return "form";
7559
+ case "phone_call":
7560
+ return "call";
7561
+ case "widget_conversation":
7562
+ return "website chat";
7563
+ case "sms":
7564
+ return "text";
7565
+ case "whatsapp":
7566
+ return "WhatsApp";
7567
+ case "email_reply":
7568
+ return "email reply";
7569
+ default:
7570
+ return kind;
7571
+ }
7572
+ }
7409
7573
  function printLead(lead) {
7410
7574
  console.log(`lead ${lead.canonical_lead_id}`);
7411
7575
  console.log(`reference ${lead.reference}`);
@@ -7419,6 +7583,16 @@ function printLead(lead) {
7419
7583
  if (lead.absorbed_lead_ids?.length) {
7420
7584
  console.log(`absorbed ${lead.absorbed_lead_ids.join(", ")}`);
7421
7585
  }
7586
+ if (lead.details?.length) {
7587
+ console.log("\nwhat they told us");
7588
+ printAlignedTable(
7589
+ ["question", "answer", "from", "when"],
7590
+ lead.details.flatMap((d) => [
7591
+ [d.label, d.value, leadDetailSource(d.source_kind), d.observed_at],
7592
+ ...(d.earlier ?? []).map((e) => ["", ` earlier: ${e.value}`, leadDetailSource(e.source_kind), e.observed_at])
7593
+ ])
7594
+ );
7595
+ }
7422
7596
  if (lead.aliases?.length) {
7423
7597
  console.log("\ncontact evidence");
7424
7598
  printAlignedTable(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.105.0",
3
+ "version": "0.108.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {