@erdoai/cli 0.75.0 → 0.77.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 +158 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
5
  import { basename } from "path";
6
6
  import { select } from "@inquirer/prompts";
7
- import { Command } from "commander";
7
+ import { Command as Command2 } from "commander";
8
8
 
9
9
  // src/config.ts
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -482,6 +482,32 @@ var ErdoClient = class {
482
482
  getVoiceCall(callID) {
483
483
  return this.request("GET", `/v1/voice/calls/${encodeURIComponent(callID)}`);
484
484
  }
485
+ // Website concierge widget conversations — the text/voice/video sessions
486
+ // visitors held on the customer's own pages. Covers every widget in the org
487
+ // unless narrowed by the widget handle (its name or public key, vw_...).
488
+ listWidgetConversations(params) {
489
+ const q = new URLSearchParams();
490
+ if (params?.widget) q.set("widget", params.widget);
491
+ if (params?.since) q.set("since", params.since);
492
+ if (params?.until) q.set("until", params.until);
493
+ if (params?.limit !== void 0) q.set("limit", String(params.limit));
494
+ if (params?.offset !== void 0) q.set("offset", String(params.offset));
495
+ if (params?.cursor) q.set("cursor", params.cursor);
496
+ const qs = q.toString();
497
+ return this.request(
498
+ "GET",
499
+ `/v1/voice/widget-conversations${qs ? `?${qs}` : ""}`
500
+ );
501
+ }
502
+ // The session id identifies the conversation on its own and another org's
503
+ // session is never returned, so the widget handle is optional.
504
+ getWidgetConversation(sessionID, widget) {
505
+ return this.request(
506
+ "POST",
507
+ `/v1/voice/widget-conversations/get`,
508
+ { session_id: sessionID, ...widget ? { widget } : {} }
509
+ );
510
+ }
485
511
  // Run a read-only HogQL query against the org's page-analytics events. Rows are
486
512
  // positional per columns; enabled:false means page analytics is off for the org
487
513
  // (not zero traffic). A rejected query surfaces PostHog's message as the error.
@@ -1556,6 +1582,32 @@ function readMaybeFile(value) {
1556
1582
  return value.startsWith("@") ? readFileSync2(value.slice(1), "utf8") : value;
1557
1583
  }
1558
1584
 
1585
+ // src/scope.ts
1586
+ import { Option } from "commander";
1587
+ var ORG_ENV = "ERDO_ORG";
1588
+ var PROJECT_ENV = "ERDO_PROJECT";
1589
+ function bindScopeFlags(program2) {
1590
+ program2.option("--org <idOrSlug>", "org to target for this command (overrides the active org)").option(
1591
+ "--project <slugOrId>",
1592
+ "project context for this command \u2014 slug or UUID (must belong to the active org)"
1593
+ );
1594
+ program2.on("option:org", (value) => {
1595
+ process.env[ORG_ENV] = value;
1596
+ });
1597
+ program2.on("option:project", (value) => {
1598
+ process.env[PROJECT_ENV] = value;
1599
+ });
1600
+ return program2;
1601
+ }
1602
+ function projectOption(description, opts = {}) {
1603
+ const option = new Option("--project <slug>", description).env(PROJECT_ENV);
1604
+ if (opts.mandatory) option.makeOptionMandatory();
1605
+ return option;
1606
+ }
1607
+ function orgOption(description) {
1608
+ return new Option("--org <idOrSlug>", description).env(ORG_ENV);
1609
+ }
1610
+
1559
1611
  // src/update.ts
1560
1612
  import { spawn as spawn2, spawnSync } from "child_process";
1561
1613
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
@@ -1770,14 +1822,9 @@ function compactEvalRunAttribution(run) {
1770
1822
  ].filter((part) => part !== void 0);
1771
1823
  return [run.triggered_by, ...revisions].join(" ");
1772
1824
  }
1773
- var program = new Command();
1774
- program.name("erdo").description("Erdo CLI").version(pkg.version).option("--org <idOrSlug>", "org to target for this command (overrides the active org)").option("--project <uuid>", "project context for this command (must belong to the active org)");
1775
- program.hook("preAction", (thisCommand) => {
1776
- const opts = thisCommand.opts();
1777
- const org2 = opts.org;
1778
- if (org2) process.env.ERDO_ORG = org2;
1779
- if (opts.project) process.env.ERDO_PROJECT = opts.project;
1780
- });
1825
+ var program = new Command2();
1826
+ program.name("erdo").description("Erdo CLI").version(pkg.version);
1827
+ bindScopeFlags(program);
1781
1828
  async function doLogin(opts) {
1782
1829
  let token;
1783
1830
  let me;
@@ -2144,7 +2191,9 @@ This key operates every org you manage; target one with X-Organization-ID (or \`
2144
2191
  }
2145
2192
  });
2146
2193
  var tokenCmd = program.command("token").description("Manage your API tokens (account-level credentials)");
2147
- tokenCmd.command("create").description("Mint a new API token; the secret is printed ONCE").requiredOption("--name <name>", "a label for the token").option("--expires-days <n>", "days until expiry (default: 30)", (v) => parseInt(v, 10)).option("--org <idOrSlug>", "the token's default org (defaults to your active org; must be one you belong to)").action(async (opts) => {
2194
+ tokenCmd.command("create").description("Mint a new API token; the secret is printed ONCE").requiredOption("--name <name>", "a label for the token").option("--expires-days <n>", "days until expiry (default: 30)", (v) => parseInt(v, 10)).addOption(
2195
+ orgOption("the token's default org (defaults to your active org; must be one you belong to)")
2196
+ ).action(async (opts) => {
2148
2197
  try {
2149
2198
  const res = await new ErdoClient().createToken({
2150
2199
  name: opts.name,
@@ -2246,7 +2295,7 @@ wsCmd.command("get <slug>").description("Show a workstream \u2014 status, phases
2246
2295
  fail(e);
2247
2296
  }
2248
2297
  });
2249
- wsCmd.command("create").description("Create a workstream in a project").requiredOption("--project <slug>", "project slug").requiredOption("--slug <slug>", "workstream slug (unique per org)").requiredOption("--title <title>", "human-readable title").option("--description <text>", "free-form description").option(
2298
+ wsCmd.command("create").description("Create a workstream in a project").addOption(projectOption("project slug", { mandatory: true })).requiredOption("--slug <slug>", "workstream slug (unique per org)").requiredOption("--title <title>", "human-readable title").option("--description <text>", "free-form description").option(
2250
2299
  "--token-budget <millicents>",
2251
2300
  "spend ceiling in millicents \u2014 100,000 per dollar, so 10000000 = $100 and 25000000 = $250; omit for unlimited",
2252
2301
  (v) => parseInt(v, 10)
@@ -2540,7 +2589,7 @@ expCmd.command("policy <slug>").description(
2540
2589
  fail(e);
2541
2590
  }
2542
2591
  });
2543
- expCmd.command("create").description("Create an experiment with its variants, evidence datasets and decision rule").requiredOption("--project <slug>", "project slug").requiredOption("--slug <slug>", "experiment slug (unique per org)").requiredOption("--title <title>", "title").option("--workstream <slug>", "host workstream").option("--scope <scope>", "local (default), platform_canary, platform_global").option("--hypothesis <text>", "hypothesis markdown").option("--primary-metric <metric>", "the metric the decision rule reads").option("--decision-rule <text>", "the rule that decides ship/stop from the primary metric").option("--guardrail <metric>", "a metric that must not regress (repeatable)", collect, []).option(
2592
+ expCmd.command("create").description("Create an experiment with its variants, evidence datasets and decision rule").addOption(projectOption("project slug", { mandatory: true })).requiredOption("--slug <slug>", "experiment slug (unique per org)").requiredOption("--title <title>", "title").option("--workstream <slug>", "host workstream").option("--scope <scope>", "local (default), platform_canary, platform_global").option("--hypothesis <text>", "hypothesis markdown").option("--primary-metric <metric>", "the metric the decision rule reads").option("--decision-rule <text>", "the rule that decides ship/stop from the primary metric").option("--guardrail <metric>", "a metric that must not regress (repeatable)", collect, []).option(
2544
2593
  "--variant <spec>",
2545
2594
  `a variant "key=b,label=3-field form,page=<artifact-id>,alloc=50,class=form_length_change,control" (repeatable). Fields: key=, label=, page= (Erdo artifact id) OR url=<https://\u2026> (an external page the persona panel visits, e.g. a client's live landing page \u2014 mutually exclusive with page=), alloc=, class=, and the bare flag control (or control=true) to mark the baseline. class= declares the structural change vs control for cross-experiment learning: headline_change | hero_media_change | cta_change | form_length_change | social_proof_add | layout_reorder | offer_change | full_page_rebuild`,
2546
2595
  collect,
@@ -2678,7 +2727,7 @@ expCmd.command("set-status <slug> <status>").description("Set an experiment's st
2678
2727
  });
2679
2728
  expCmd.command("calibration").description(
2680
2729
  "Read each judge's track record \u2014 paired predictions and pairwise agreement vs measured reality (the trust dial)"
2681
- ).option("--project <slug>", "limit to one project (default: every project you can view)").option("--judge <slug>", "show only judges whose slug contains this text").option("-n, --limit <n>", "max ledger rows scanned (default 5000)", (v) => parseInt(v, 10)).action(async (opts) => {
2730
+ ).addOption(projectOption("limit to one project (default: every project you can view)")).option("--judge <slug>", "show only judges whose slug contains this text").option("-n, --limit <n>", "max ledger rows scanned (default 5000)", (v) => parseInt(v, 10)).action(async (opts) => {
2682
2731
  try {
2683
2732
  const res = await new ErdoClient().judgeCalibration({
2684
2733
  project_slug: opts.project,
@@ -4753,7 +4802,17 @@ sentEmailsCmd.command("get <emailID>").description("Read one sent email, includi
4753
4802
  fail(e);
4754
4803
  }
4755
4804
  });
4756
- var voiceCmd = program.command("voice").description("Read voice agent phone conversations");
4805
+ var voiceCmd = program.command("voice").description("Read voice agent phone calls and website widget conversations");
4806
+ function contactLabel(contact) {
4807
+ if (!contact) return "-";
4808
+ const name = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
4809
+ const reach = contact.email || contact.phone;
4810
+ if (!name && !reach) return "none";
4811
+ return [name, reach].filter(Boolean).join(" \xB7 ");
4812
+ }
4813
+ function leadStatusLabel(outcome) {
4814
+ return outcome ? outcome : "pending";
4815
+ }
4757
4816
  var voiceCallsCmd = voiceCmd.command("calls").description("Inbound and outbound phone call records");
4758
4817
  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(
4759
4818
  async (opts) => {
@@ -4775,7 +4834,19 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
4775
4834
  return;
4776
4835
  }
4777
4836
  printAlignedTable(
4778
- ["call id", "direction", "from", "to", "status", "secs", "transcript", "started", "summary"],
4837
+ [
4838
+ "call id",
4839
+ "direction",
4840
+ "from",
4841
+ "to",
4842
+ "status",
4843
+ "secs",
4844
+ "transcript",
4845
+ "contact",
4846
+ "lead",
4847
+ "started",
4848
+ "summary"
4849
+ ],
4779
4850
  calls.map((call) => [
4780
4851
  call.call_id,
4781
4852
  call.direction,
@@ -4784,6 +4855,8 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
4784
4855
  call.status,
4785
4856
  call.duration_seconds,
4786
4857
  call.has_transcript ? "yes" : "no",
4858
+ contactLabel(call.contact),
4859
+ leadStatusLabel(call.lead_status),
4787
4860
  call.created_at,
4788
4861
  call.transcript_summary
4789
4862
  ])
@@ -4799,13 +4872,82 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
4799
4872
  }
4800
4873
  }
4801
4874
  );
4802
- voiceCallsCmd.command("get <callID>").description("Read one phone call in full: transcript, summary, and per-turn LLM metrics").action(async (callID) => {
4875
+ voiceCallsCmd.command("get <callID>").description(
4876
+ "Read one phone call in full: transcript, summary, per-turn LLM metrics, and the lead it produced (contact, lead_status, lead_saved_at)"
4877
+ ).action(async (callID) => {
4803
4878
  try {
4804
4879
  print(await new ErdoClient().getVoiceCall(callID));
4805
4880
  } catch (e) {
4806
4881
  fail(e);
4807
4882
  }
4808
4883
  });
4884
+ var widgetConversationsCmd = voiceCmd.command("widget-conversations").description("Website concierge widget conversations (text, voice and video sessions)");
4885
+ 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(
4886
+ async (opts) => {
4887
+ try {
4888
+ const res = await new ErdoClient().listWidgetConversations({
4889
+ widget: opts.widget,
4890
+ since: opts.since,
4891
+ until: opts.until,
4892
+ limit: opts.limit ? Number(opts.limit) : void 0,
4893
+ offset: opts.offset ? Number(opts.offset) : void 0,
4894
+ cursor: opts.cursor
4895
+ });
4896
+ if (opts.json) {
4897
+ print(res);
4898
+ return;
4899
+ }
4900
+ const conversations = res.conversations ?? [];
4901
+ if (conversations.length === 0) {
4902
+ console.log("No conversations match those filters.");
4903
+ return;
4904
+ }
4905
+ printAlignedTable(
4906
+ [
4907
+ "session id",
4908
+ "widget",
4909
+ "modality",
4910
+ "secs",
4911
+ "transcript",
4912
+ "contact",
4913
+ "lead",
4914
+ "started",
4915
+ "summary"
4916
+ ],
4917
+ conversations.map((c) => [
4918
+ c.session_id,
4919
+ c.widget_name || c.widget,
4920
+ c.modality,
4921
+ c.duration_seconds,
4922
+ c.has_transcript ? "yes" : "no",
4923
+ contactLabel(c.contact),
4924
+ leadStatusLabel(c.lead_status),
4925
+ c.created_at,
4926
+ c.transcript_summary
4927
+ ])
4928
+ );
4929
+ process.stderr.write(
4930
+ `showing ${conversations.length} of ${res.total} conversation(s)
4931
+ `
4932
+ );
4933
+ if (res.next_cursor) {
4934
+ process.stderr.write(`more available \u2014 re-run with --cursor ${res.next_cursor}
4935
+ `);
4936
+ }
4937
+ } catch (e) {
4938
+ fail(e);
4939
+ }
4940
+ }
4941
+ );
4942
+ widgetConversationsCmd.command("get <sessionID>").description(
4943
+ "Read one widget conversation in full: transcript, summary, topics, per-turn LLM metrics, and the lead it produced (contact, lead_status, lead_saved_at)"
4944
+ ).option("--widget <handle>", "the widget it belongs to \u2014 optional, the session id identifies it").action(async (sessionID, opts) => {
4945
+ try {
4946
+ print(await new ErdoClient().getWidgetConversation(sessionID, opts.widget));
4947
+ } catch (e) {
4948
+ fail(e);
4949
+ }
4950
+ });
4809
4951
  var datasetsCmd = program.command("datasets").description("Datasets");
4810
4952
  datasetsCmd.command("list").description("List datasets").option(
4811
4953
  "--class <class>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.75.0",
3
+ "version": "0.77.0",
4
4
  "description": "Erdo CLI \u2014 drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {