@erdoai/cli 0.88.0 → 0.90.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 +100 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -489,10 +489,13 @@ var ErdoClient = class {
489
489
  // Phone conversation records: inbound calls to a voice agent's own number and
490
490
  // the outbound calls Erdo placed. Website chat widget conversations are a
491
491
  // different resource — they live under /v1/voice/widget-conversations.
492
+ // canonical_lead_id narrows to one lead's calls, by its UUID or its lead
493
+ // reference — the same filter every conversation list takes.
492
494
  listVoiceCalls(params) {
493
495
  const q = new URLSearchParams();
494
496
  if (params?.agent) q.set("agent", params.agent);
495
497
  if (params?.direction) q.set("direction", params.direction);
498
+ if (params?.canonical_lead_id) q.set("canonical_lead_id", params.canonical_lead_id);
496
499
  if (params?.limit !== void 0) q.set("limit", String(params.limit));
497
500
  if (params?.offset !== void 0) q.set("offset", String(params.offset));
498
501
  if (params?.cursor) q.set("cursor", params.cursor);
@@ -510,6 +513,7 @@ var ErdoClient = class {
510
513
  if (params?.widget) q.set("widget", params.widget);
511
514
  if (params?.since) q.set("since", params.since);
512
515
  if (params?.until) q.set("until", params.until);
516
+ if (params?.canonical_lead_id) q.set("canonical_lead_id", params.canonical_lead_id);
513
517
  if (params?.limit !== void 0) q.set("limit", String(params.limit));
514
518
  if (params?.offset !== void 0) q.set("offset", String(params.offset));
515
519
  if (params?.cursor) q.set("cursor", params.cursor);
@@ -536,6 +540,7 @@ var ErdoClient = class {
536
540
  const q = new URLSearchParams();
537
541
  if (params?.phone_number) q.set("phone_number", params.phone_number);
538
542
  if (params?.address) q.set("address", params.address);
543
+ if (params?.canonical_lead_id) q.set("canonical_lead_id", params.canonical_lead_id);
539
544
  if (params?.limit !== void 0) q.set("limit", String(params.limit));
540
545
  if (params?.cursor) q.set("cursor", params.cursor);
541
546
  const qs = q.toString();
@@ -1153,6 +1158,23 @@ var ErdoClient = class {
1153
1158
  `/v1/datasets/${encodeURIComponent(slug)}/refresh/run`
1154
1159
  );
1155
1160
  }
1161
+ // --- lead identity ---
1162
+ // Preview or perform a dataset's permanent-lead-identity migration: give every
1163
+ // pipeline writer the producer contract, backfill the IDs, and publish the
1164
+ // identity config in one transaction.
1165
+ //
1166
+ // apply:false mutates nothing and is the way to read readiness — `ready`, the
1167
+ // `blockers` holding it up, and how many rows would be assigned an identity.
1168
+ // apply:true needs an operation_key: replaying the same key settles an apply
1169
+ // whose response was lost, while a different key against a migrated dataset is
1170
+ // refused rather than migrating twice.
1171
+ maintainLeadIdentity(slug, body) {
1172
+ return this.request(
1173
+ "POST",
1174
+ `/v1/datasets/${encodeURIComponent(slug)}/lead-identity/maintain`,
1175
+ body
1176
+ );
1177
+ }
1156
1178
  // --- bounded outreach ---
1157
1179
  putOutreachBatch(batch, body) {
1158
1180
  return this.request(
@@ -5198,13 +5220,18 @@ function contactLabel(contact) {
5198
5220
  function leadStatusLabel(outcome) {
5199
5221
  return outcome ? outcome : "pending";
5200
5222
  }
5223
+ function leadReferenceLabel(reference) {
5224
+ return reference ? reference : "-";
5225
+ }
5226
+ var LEAD_FILTER_HELP = "only this lead's conversations \u2014 its canonical lead id (UUID) or its 22-character lead reference";
5201
5227
  var voiceCallsCmd = voiceCmd.command("calls").description("Inbound and outbound phone call records");
5202
- voiceCallsCmd.command("list").description("List the organization's phone calls, newest first").option("--agent <slug>", "only calls held by this voice agent, by slug").option("--direction <direction>", "only 'inbound' (calls to the agent's number) or 'outbound'").option("--limit <n>", "page size (default 25, max 100)").option("--offset <n>", "rows to skip").option("--cursor <cursor>", "next_cursor from the preceding page (stable paging)").option("--json", "print the raw JSON result instead of a table").action(
5228
+ voiceCallsCmd.command("list").description("List the organization's phone calls, newest first").option("--agent <slug>", "only calls held by this voice agent, by slug").option("--direction <direction>", "only 'inbound' (calls to the agent's number) or 'outbound'").option("--lead <lead>", LEAD_FILTER_HELP).option("--limit <n>", "page size (default 25, max 100)").option("--offset <n>", "rows to skip").option("--cursor <cursor>", "next_cursor from the preceding page (stable paging)").option("--json", "print the raw JSON result instead of a table").action(
5203
5229
  async (opts) => {
5204
5230
  try {
5205
5231
  const res = await new ErdoClient().listVoiceCalls({
5206
5232
  agent: opts.agent,
5207
5233
  direction: opts.direction,
5234
+ canonical_lead_id: opts.lead,
5208
5235
  limit: opts.limit ? Number(opts.limit) : void 0,
5209
5236
  offset: opts.offset ? Number(opts.offset) : void 0,
5210
5237
  cursor: opts.cursor
@@ -5229,6 +5256,7 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
5229
5256
  "transcript",
5230
5257
  "contact",
5231
5258
  "lead",
5259
+ "lead ref",
5232
5260
  "started",
5233
5261
  "summary"
5234
5262
  ],
@@ -5242,6 +5270,7 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
5242
5270
  call.has_transcript ? "yes" : "no",
5243
5271
  contactLabel(call.contact),
5244
5272
  leadStatusLabel(call.lead_status),
5273
+ leadReferenceLabel(call.lead_reference),
5245
5274
  call.created_at,
5246
5275
  call.transcript_summary
5247
5276
  ])
@@ -5258,7 +5287,7 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
5258
5287
  }
5259
5288
  );
5260
5289
  voiceCallsCmd.command("get <callID>").description(
5261
- "Read one phone call in full: transcript, summary, per-turn LLM metrics, and the lead it produced (contact, lead_status, lead_saved_at)"
5290
+ "Read one phone call in full: transcript, summary, per-turn LLM metrics, and the lead it produced (contact, lead_status, lead_saved_at, canonical_lead_id, lead_reference)"
5262
5291
  ).action(async (callID) => {
5263
5292
  try {
5264
5293
  print(await new ErdoClient().getVoiceCall(callID));
@@ -5267,13 +5296,14 @@ voiceCallsCmd.command("get <callID>").description(
5267
5296
  }
5268
5297
  });
5269
5298
  var widgetConversationsCmd = voiceCmd.command("widget-conversations").description("Website concierge widget conversations (text, voice and video sessions)");
5270
- widgetConversationsCmd.command("list").description("List the organization's widget conversations, newest first").option("--widget <handle>", "only this widget's conversations \u2014 its name or public key (vw_...)").option("--since <rfc3339>", "only conversations that started at or after this time (inclusive)").option("--until <rfc3339>", "only conversations that started before this time (exclusive)").option("--limit <n>", "page size (default 25, max 100)").option("--offset <n>", "rows to skip").option("--cursor <cursor>", "next_cursor from the preceding page (stable paging)").option("--json", "print the raw JSON result instead of a table").action(
5299
+ widgetConversationsCmd.command("list").description("List the organization's widget conversations, newest first").option("--widget <handle>", "only this widget's conversations \u2014 its name or public key (vw_...)").option("--since <rfc3339>", "only conversations that started at or after this time (inclusive)").option("--until <rfc3339>", "only conversations that started before this time (exclusive)").option("--lead <lead>", LEAD_FILTER_HELP).option("--limit <n>", "page size (default 25, max 100)").option("--offset <n>", "rows to skip").option("--cursor <cursor>", "next_cursor from the preceding page (stable paging)").option("--json", "print the raw JSON result instead of a table").action(
5271
5300
  async (opts) => {
5272
5301
  try {
5273
5302
  const res = await new ErdoClient().listWidgetConversations({
5274
5303
  widget: opts.widget,
5275
5304
  since: opts.since,
5276
5305
  until: opts.until,
5306
+ canonical_lead_id: opts.lead,
5277
5307
  limit: opts.limit ? Number(opts.limit) : void 0,
5278
5308
  offset: opts.offset ? Number(opts.offset) : void 0,
5279
5309
  cursor: opts.cursor
@@ -5296,6 +5326,7 @@ widgetConversationsCmd.command("list").description("List the organization's widg
5296
5326
  "transcript",
5297
5327
  "contact",
5298
5328
  "lead",
5329
+ "lead ref",
5299
5330
  "started",
5300
5331
  "summary"
5301
5332
  ],
@@ -5307,6 +5338,7 @@ widgetConversationsCmd.command("list").description("List the organization's widg
5307
5338
  c.has_transcript ? "yes" : "no",
5308
5339
  contactLabel(c.contact),
5309
5340
  leadStatusLabel(c.lead_status),
5341
+ leadReferenceLabel(c.lead_reference),
5310
5342
  c.created_at,
5311
5343
  c.transcript_summary
5312
5344
  ])
@@ -5325,7 +5357,7 @@ widgetConversationsCmd.command("list").description("List the organization's widg
5325
5357
  }
5326
5358
  );
5327
5359
  widgetConversationsCmd.command("get <sessionID>").description(
5328
- "Read one widget conversation in full: transcript, summary, topics, per-turn LLM metrics, and the lead it produced (contact, lead_status, lead_saved_at)"
5360
+ "Read one widget conversation in full: transcript, summary, topics, per-turn LLM metrics, and the lead it produced (contact, lead_status, lead_saved_at, canonical_lead_id, lead_reference)"
5329
5361
  ).option("--widget <handle>", "the widget it belongs to \u2014 optional, the session id identifies it").action(async (sessionID, opts) => {
5330
5362
  try {
5331
5363
  print(await new ErdoClient().getWidgetConversation(sessionID, opts.widget));
@@ -5334,12 +5366,13 @@ widgetConversationsCmd.command("get <sessionID>").description(
5334
5366
  }
5335
5367
  });
5336
5368
  var voiceSMSCmd = voiceCmd.command("sms").description("Text conversations on a voice agent's own number");
5337
- voiceSMSCmd.command("list").description("List the organization's SMS 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("--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(
5369
+ voiceSMSCmd.command("list").description("List the organization's SMS 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(
5338
5370
  async (opts) => {
5339
5371
  try {
5340
5372
  const res = await new ErdoClient().listSMSConversations({
5341
5373
  phone_number: opts.phoneNumber,
5342
5374
  address: opts.address,
5375
+ canonical_lead_id: opts.lead,
5343
5376
  limit: opts.limit ? Number(opts.limit) : void 0,
5344
5377
  cursor: opts.cursor
5345
5378
  });
@@ -5353,7 +5386,7 @@ voiceSMSCmd.command("list").description("List the organization's SMS conversatio
5353
5386
  return;
5354
5387
  }
5355
5388
  printAlignedTable(
5356
- ["session id", "phone", "address", "msgs", "last inbound", "last outbound", "summary"],
5389
+ ["session id", "phone", "address", "msgs", "last inbound", "last outbound", "lead ref", "summary"],
5357
5390
  conversations.map((c) => [
5358
5391
  c.session_id,
5359
5392
  c.phone_number,
@@ -5363,6 +5396,7 @@ voiceSMSCmd.command("list").description("List the organization's SMS conversatio
5363
5396
  c.message_count,
5364
5397
  c.last_inbound_at ?? "",
5365
5398
  c.last_outbound_at ?? "",
5399
+ leadReferenceLabel(c.lead_reference),
5366
5400
  c.summary
5367
5401
  ])
5368
5402
  );
@@ -5409,6 +5443,9 @@ voiceSMSCmd.command("get <sessionID>").description("Read one SMS conversation: i
5409
5443
  console.log(`
5410
5444
  contact: ${parts.join(" \xB7 ")}`);
5411
5445
  }
5446
+ if (c.lead_reference) {
5447
+ console.log(`lead: ${c.lead_reference} (${c.canonical_lead_id})`);
5448
+ }
5412
5449
  const messages = res.messages ?? [];
5413
5450
  if (messages.length === 0) {
5414
5451
  console.log("\nNo texts have been recorded in this conversation yet.");
@@ -5653,6 +5690,49 @@ datasetsCmd.command("set-purpose <slug> [purpose]").description(
5653
5690
  fail(e);
5654
5691
  }
5655
5692
  });
5693
+ var leadIdentityCmd = datasetsCmd.command("lead-identity").description(
5694
+ "Permanent lead identity \u2014 one lead is one row, however many ways they got in touch. Migrating a dataset gives every row a canonical_lead_id, points every pipeline writer at it, and makes the resolver the only thing that decides whether a submission is a new lead or an existing one."
5695
+ );
5696
+ leadIdentityCmd.command("maintain <slug>").description(
5697
+ "Preview or perform the migration for one dataset. Without --apply it mutates nothing and reports readiness: whether the dataset is ready, what blocks it, and how many rows would be assigned an identity. Run the preview first and read the blockers \u2014 apply refuses outright while any consumer is incompatible, unverifiable, queued or running, because the backfill rewrites the file every one of them reads."
5698
+ ).option(
5699
+ "--apply",
5700
+ "perform the migration rather than preview it; requires --operation-key"
5701
+ ).option(
5702
+ "--operation-key <key>",
5703
+ "the migration receipt. Replaying the same key settles an apply whose response was lost; a different key against an already-migrated dataset is refused rather than migrating it twice."
5704
+ ).option("--email-column <column>", "the dataset's email column (detected when omitted)").option("--phone-column <column>", "the dataset's phone column (detected when omitted)").option(
5705
+ "--conversion-id-column <column>",
5706
+ "optional conversion id column; must differ from both contact columns"
5707
+ ).option(
5708
+ "--default-calling-code <code>",
5709
+ "calling code a phone written in national form belongs to, so one number written two ways is one lead (default 1)"
5710
+ ).action(
5711
+ async (slug, opts) => {
5712
+ try {
5713
+ if (opts.apply && !opts.operationKey) {
5714
+ fail(
5715
+ new Error(
5716
+ "--apply requires --operation-key so an uncertain retry can settle rather than migrating twice"
5717
+ )
5718
+ );
5719
+ return;
5720
+ }
5721
+ print(
5722
+ await new ErdoClient().maintainLeadIdentity(slug, {
5723
+ apply: Boolean(opts.apply),
5724
+ operation_key: opts.operationKey,
5725
+ email_column: opts.emailColumn,
5726
+ phone_column: opts.phoneColumn,
5727
+ conversion_id_column: opts.conversionIdColumn,
5728
+ default_calling_code: opts.defaultCallingCode
5729
+ })
5730
+ );
5731
+ } catch (e) {
5732
+ fail(e);
5733
+ }
5734
+ }
5735
+ );
5656
5736
  datasetsCmd.command("query <slug> <question>").description(
5657
5737
  "Ask a natural-language question of a dataset \u2014 Erdo writes and runs the SQL, and answers with that SQL alongside the values. It runs an agent, so it is slower and two identical questions can produce two different queries: for a deterministic or scripted read, write the SQL yourself with `datasets fetch --sql`."
5658
5738
  ).action(async (slug, question) => {
@@ -6230,8 +6310,20 @@ function printLead(lead) {
6230
6310
  if (lead.captures?.length) {
6231
6311
  console.log("\ncaptures");
6232
6312
  printAlignedTable(
6233
- ["when", "producer", "resolution", "conversion"],
6234
- lead.captures.map((c) => [c.created_at, c.producer, c.resolution, c.conversion_status])
6313
+ ["when", "producer", "pipeline", "resolution", "conversion"],
6314
+ lead.captures.map((c) => [c.created_at, c.producer, c.pipeline_id ?? "", c.resolution, c.conversion_status])
6315
+ );
6316
+ }
6317
+ if (lead.refused_captures?.length) {
6318
+ console.log("\nrefused captures (this lead was one of the candidates)");
6319
+ printAlignedTable(
6320
+ ["when", "pipeline", "resolution", "candidates"],
6321
+ lead.refused_captures.map((c) => [
6322
+ c.created_at,
6323
+ c.pipeline_id ?? "",
6324
+ c.resolution,
6325
+ (c.candidate_lead_ids ?? []).join(", ")
6326
+ ])
6235
6327
  );
6236
6328
  }
6237
6329
  if (lead.row) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.88.0",
3
+ "version": "0.90.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {