@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.
- package/dist/index.js +158 -16
- 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
|
|
1774
|
-
program.name("erdo").description("Erdo CLI").version(pkg.version)
|
|
1775
|
-
program
|
|
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)).
|
|
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").
|
|
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").
|
|
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
|
-
).
|
|
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
|
-
[
|
|
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(
|
|
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>",
|