@erdoai/cli 0.91.0 → 0.99.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 +903 -59
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -312,6 +312,19 @@ var ErdoClient = class {
|
|
|
312
312
|
createManagerKey() {
|
|
313
313
|
return this.request("POST", "/v1/manager-key");
|
|
314
314
|
}
|
|
315
|
+
// Daily paid-media and lead performance for one organization, or for every
|
|
316
|
+
// client org this account manages when organization_slug is omitted.
|
|
317
|
+
managerPerformance(params) {
|
|
318
|
+
const qs = new URLSearchParams();
|
|
319
|
+
if (params.organization_slug) qs.set("organization_slug", params.organization_slug);
|
|
320
|
+
if (params.from) qs.set("from", params.from);
|
|
321
|
+
if (params.to) qs.set("to", params.to);
|
|
322
|
+
const q = qs.toString();
|
|
323
|
+
return this.request(
|
|
324
|
+
"GET",
|
|
325
|
+
`/v1/managed-organization-performance${q ? `?${q}` : ""}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
315
328
|
// Brings an EXISTING org under your manager org. Two consents redeem here: a
|
|
316
329
|
// one-time token minted by the target org's owner (or, for an ownerless managed
|
|
317
330
|
// org, its current manager) — a manager credential on its own can never adopt an
|
|
@@ -464,6 +477,8 @@ var ErdoClient = class {
|
|
|
464
477
|
const q = new URLSearchParams();
|
|
465
478
|
if (params?.domain) q.set("domain", params.domain);
|
|
466
479
|
if (params?.limit) q.set("limit", String(params.limit));
|
|
480
|
+
if (params?.leadRef) q.set("lead_ref", params.leadRef);
|
|
481
|
+
if (params?.dataset) q.set("dataset", params.dataset);
|
|
467
482
|
const qs = q.toString();
|
|
468
483
|
return this.request(
|
|
469
484
|
"GET",
|
|
@@ -604,6 +619,16 @@ var ErdoClient = class {
|
|
|
604
619
|
{ enabled }
|
|
605
620
|
);
|
|
606
621
|
}
|
|
622
|
+
// A separate switch from the text one, on the same number: the two are
|
|
623
|
+
// separate permissions, so one endpoint per channel rather than a channel
|
|
624
|
+
// argument that could move the wrong one.
|
|
625
|
+
setVoiceAgentWhatsAppReplies(slug, enabled) {
|
|
626
|
+
return this.request(
|
|
627
|
+
"POST",
|
|
628
|
+
`/v1/voice/agents/${encodeURIComponent(slug)}/whatsapp-replies`,
|
|
629
|
+
{ enabled }
|
|
630
|
+
);
|
|
631
|
+
}
|
|
607
632
|
// Run a read-only HogQL query against the org's page-analytics events. Rows are
|
|
608
633
|
// positional per columns; enabled:false means page analytics is off for the org
|
|
609
634
|
// (not zero traffic). A rejected query surfaces PostHog's message as the error.
|
|
@@ -884,6 +909,9 @@ var ErdoClient = class {
|
|
|
884
909
|
for (const s of params?.statuses ?? []) q.append("statuses", s);
|
|
885
910
|
if (params?.engine_actions_only) q.set("engine_actions_only", "true");
|
|
886
911
|
if (params?.item_id) q.set("item_id", params.item_id);
|
|
912
|
+
if (params?.organization_slug) q.set("organization_slug", params.organization_slug);
|
|
913
|
+
if (params?.subject_kind) q.set("subject_kind", params.subject_kind);
|
|
914
|
+
if (params?.subject_id) q.set("subject_id", params.subject_id);
|
|
887
915
|
if (params?.limit) q.set("limit", String(params.limit));
|
|
888
916
|
if (params?.offset) q.set("offset", String(params.offset));
|
|
889
917
|
const qs = q.toString();
|
|
@@ -1128,6 +1156,85 @@ var ErdoClient = class {
|
|
|
1128
1156
|
input
|
|
1129
1157
|
);
|
|
1130
1158
|
}
|
|
1159
|
+
// The lead playbook: plain text that decides each lead's next action. An
|
|
1160
|
+
// organization that never saved one gets the default template, exists=false.
|
|
1161
|
+
getLeadPlaybook() {
|
|
1162
|
+
return this.request("GET", "/v1/lead-playbook");
|
|
1163
|
+
}
|
|
1164
|
+
// Saving reads the text into the typed read-back and stores both. enabled and
|
|
1165
|
+
// daily_contact_cap keep their saved values when omitted. Org admins only.
|
|
1166
|
+
// agent_id links the concierge every draft is written as. Unlike enabled and
|
|
1167
|
+
// daily_contact_cap it is NOT sticky: omitting it clears the link.
|
|
1168
|
+
putLeadPlaybook(input) {
|
|
1169
|
+
return this.request("PUT", "/v1/lead-playbook", input);
|
|
1170
|
+
}
|
|
1171
|
+
// The read-back of a draft, without saving it.
|
|
1172
|
+
readLeadPlaybook(body, agentID) {
|
|
1173
|
+
return this.request("POST", "/v1/lead-playbook/read-back", { body, agent_id: agentID });
|
|
1174
|
+
}
|
|
1175
|
+
// Every saved version of the playbook, newest first. Read-only: a revision is
|
|
1176
|
+
// written once, inside the save's own transaction.
|
|
1177
|
+
listLeadPlaybookRevisions(params) {
|
|
1178
|
+
const q = new URLSearchParams();
|
|
1179
|
+
if (params?.limit) q.set("limit", String(params.limit));
|
|
1180
|
+
if (params?.offset) q.set("offset", String(params.offset));
|
|
1181
|
+
const qs = q.toString();
|
|
1182
|
+
return this.request("GET", `/v1/lead-playbook/revisions${qs ? `?${qs}` : ""}`);
|
|
1183
|
+
}
|
|
1184
|
+
// One saved revision in full — what explains a decision whose
|
|
1185
|
+
// playbook_revision names it, long after the playbook has moved on.
|
|
1186
|
+
getLeadPlaybookRevision(revision) {
|
|
1187
|
+
return this.request("GET", `/v1/lead-playbook/revisions/${encodeURIComponent(String(revision))}`);
|
|
1188
|
+
}
|
|
1189
|
+
// The organization's concierges, for linking one to the playbook.
|
|
1190
|
+
listVoiceAgents() {
|
|
1191
|
+
return this.request("GET", "/v1/voice/agents");
|
|
1192
|
+
}
|
|
1193
|
+
// Each lead's current suggestion, highest priority first.
|
|
1194
|
+
listLeadNextActions(params) {
|
|
1195
|
+
const q = new URLSearchParams();
|
|
1196
|
+
if (params?.dataset) q.set("dataset", params.dataset);
|
|
1197
|
+
if (params?.status) q.set("status", params.status);
|
|
1198
|
+
if (params?.priority) q.set("priority", params.priority);
|
|
1199
|
+
if (params?.limit) q.set("limit", String(params.limit));
|
|
1200
|
+
if (params?.offset) q.set("offset", String(params.offset));
|
|
1201
|
+
const qs = q.toString();
|
|
1202
|
+
return this.request("GET", `/v1/lead-next-actions${qs ? `?${qs}` : ""}`);
|
|
1203
|
+
}
|
|
1204
|
+
// Every decision about one lead, newest first.
|
|
1205
|
+
getLeadNextActions(datasetSlug, lead) {
|
|
1206
|
+
return this.request(
|
|
1207
|
+
"GET",
|
|
1208
|
+
`/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/next-actions`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
// A person's hold or close. It replaces the lead's open suggestion and
|
|
1212
|
+
// withdraws any card that suggestion filed.
|
|
1213
|
+
recordLeadNextAction(datasetSlug, lead, input) {
|
|
1214
|
+
return this.request(
|
|
1215
|
+
"POST",
|
|
1216
|
+
`/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/next-actions`,
|
|
1217
|
+
input
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
// Everything the evaluation reads about one lead.
|
|
1221
|
+
getLeadTimeline(datasetSlug, lead) {
|
|
1222
|
+
return this.request(
|
|
1223
|
+
"GET",
|
|
1224
|
+
`/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/timeline`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
// A dry run: nothing is stored and nothing is sent. body tries a draft
|
|
1228
|
+
// playbook instead of the saved one.
|
|
1229
|
+
// agent_id tries the draft as a different concierge; it is read only
|
|
1230
|
+
// alongside body, since a dry run of the saved playbook uses its saved one.
|
|
1231
|
+
evaluateLeadNextAction(datasetSlug, lead, body, agentID) {
|
|
1232
|
+
return this.request(
|
|
1233
|
+
"POST",
|
|
1234
|
+
`/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}/next-actions/evaluate`,
|
|
1235
|
+
body ? { body, agent_id: agentID } : {}
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1131
1238
|
listDatasetRevisions(slug) {
|
|
1132
1239
|
return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
|
|
1133
1240
|
}
|
|
@@ -1982,6 +2089,29 @@ function timedOutMessage(threadID) {
|
|
|
1982
2089
|
function print(value) {
|
|
1983
2090
|
console.log(JSON.stringify(value, null, 2));
|
|
1984
2091
|
}
|
|
2092
|
+
function formatNumber(n) {
|
|
2093
|
+
if (!Number.isFinite(n)) return String(n);
|
|
2094
|
+
if (Number.isInteger(n)) return n.toLocaleString("en-US");
|
|
2095
|
+
if (Math.abs(n) < 1e-6) {
|
|
2096
|
+
return n.toFixed(10).replace(/0+$/, "").replace(/\.$/, "");
|
|
2097
|
+
}
|
|
2098
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
2099
|
+
}
|
|
2100
|
+
function formatDate(iso) {
|
|
2101
|
+
if (!iso) return void 0;
|
|
2102
|
+
const d = new Date(iso);
|
|
2103
|
+
if (isNaN(d.getTime())) return iso;
|
|
2104
|
+
return `${d.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
2105
|
+
}
|
|
2106
|
+
function compactJSON(value) {
|
|
2107
|
+
if (value === void 0 || value === null) return void 0;
|
|
2108
|
+
try {
|
|
2109
|
+
const s = JSON.stringify(value);
|
|
2110
|
+
return s === "null" ? void 0 : s;
|
|
2111
|
+
} catch {
|
|
2112
|
+
return String(value);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
1985
2115
|
async function readAllStdin() {
|
|
1986
2116
|
if (process.stdin.isTTY) {
|
|
1987
2117
|
throw new Error("nothing is piped to standard input");
|
|
@@ -2030,7 +2160,11 @@ function printPageReview(review) {
|
|
|
2030
2160
|
}
|
|
2031
2161
|
}
|
|
2032
2162
|
function printAlignedTable(columns, rows) {
|
|
2033
|
-
const cell = (v) =>
|
|
2163
|
+
const cell = (v) => {
|
|
2164
|
+
if (v === null || v === void 0) return "";
|
|
2165
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
2166
|
+
return String(v);
|
|
2167
|
+
};
|
|
2034
2168
|
const widths = columns.map((c, i) => Math.max(c.length, ...rows.map((r) => cell(r[i]).length), 0));
|
|
2035
2169
|
const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
2036
2170
|
console.log(line(columns));
|
|
@@ -2045,7 +2179,10 @@ var collect = (v, acc) => {
|
|
|
2045
2179
|
function summariseResults(results, cases = []) {
|
|
2046
2180
|
const nameByID = new Map(cases.map((c) => [c.id, c.name]));
|
|
2047
2181
|
for (const r of results) {
|
|
2048
|
-
|
|
2182
|
+
let status;
|
|
2183
|
+
if (r.agent_error) status = "ERROR";
|
|
2184
|
+
else if (r.passed) status = "PASS";
|
|
2185
|
+
else status = "FAIL";
|
|
2049
2186
|
const name = nameByID.get(r.case_id) ?? r.case_id;
|
|
2050
2187
|
console.log(` [${status}] ${r.score.toFixed(2)} ${name}`);
|
|
2051
2188
|
if (r.agent_error) console.log(` agent error: ${r.agent_error}`);
|
|
@@ -2273,6 +2410,7 @@ function printBusinessProfile(profile) {
|
|
|
2273
2410
|
["industry", profile.industry],
|
|
2274
2411
|
["privacy policy", profile.privacy_policy_url],
|
|
2275
2412
|
["terms", profile.terms_and_conditions_url],
|
|
2413
|
+
["example landing page", profile.example_landing_page_url],
|
|
2276
2414
|
["representative", `${profile.representative_first_name} ${profile.representative_last_name}`.trim()],
|
|
2277
2415
|
["title", profile.representative_title],
|
|
2278
2416
|
["job position", profile.representative_job_position],
|
|
@@ -2324,7 +2462,10 @@ businessProfileCmd.command("set").description("Save the organization's business
|
|
|
2324
2462
|
).option(
|
|
2325
2463
|
"--tax-id-stdin",
|
|
2326
2464
|
"read the tax id from standard input instead of prompting, for a script \u2014 e.g. `pass show ein | erdo org business-profile set --tax-id-stdin`"
|
|
2327
|
-
).option("--street <street>", "street address of the registered business").option("--city <city>", "city of the registered business").option("--region <region>", "state or province, e.g. FL").option("--postal-code <code>", "postal or ZIP code").option("--country <iso2>", "two-letter ISO country code, e.g. US").option("--website-url <url>", "the business's own website \u2014 carriers check that it describes it").option("--industry <industry>", "one of the industries listed by: erdo org business-profile get --json").option("--privacy-policy-url <url>", "the business's own privacy policy page (https) \u2014 a campaign without one is rejected").option("--terms-url <url>", "the business's own terms and conditions page (https) \u2014 required for the same reason").option(
|
|
2465
|
+
).option("--street <street>", "street address of the registered business").option("--city <city>", "city of the registered business").option("--region <region>", "state or province, e.g. FL").option("--postal-code <code>", "postal or ZIP code").option("--country <iso2>", "two-letter ISO country code, e.g. US").option("--website-url <url>", "the business's own website \u2014 carriers check that it describes it").option("--industry <industry>", "one of the industries listed by: erdo org business-profile get --json").option("--privacy-policy-url <url>", "the business's own privacy policy page (https) \u2014 a campaign without one is rejected").option("--terms-url <url>", "the business's own terms and conditions page (https) \u2014 required for the same reason").option(
|
|
2466
|
+
"--example-landing-page-url <url>",
|
|
2467
|
+
"a public landing page (https) where people opt in to texts through the form's SMS consent checkbox and that shows your phone number \u2014 the opt-in the campaign reviewer opens"
|
|
2468
|
+
).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(
|
|
2328
2469
|
"--job-position <position>",
|
|
2329
2470
|
"their role from the fixed list (ceo, cfo, director, general_counsel, gm, vp, other)"
|
|
2330
2471
|
).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(
|
|
@@ -2362,6 +2503,7 @@ businessProfileCmd.command("set").description("Save the organization's business
|
|
|
2362
2503
|
industry: opts.industry ?? stored.industry,
|
|
2363
2504
|
privacy_policy_url: opts.privacyPolicyUrl ?? stored.privacy_policy_url,
|
|
2364
2505
|
terms_and_conditions_url: opts.termsUrl ?? stored.terms_and_conditions_url,
|
|
2506
|
+
example_landing_page_url: opts.exampleLandingPageUrl ?? stored.example_landing_page_url ?? "",
|
|
2365
2507
|
representative_first_name: opts.firstName ?? stored.representative_first_name,
|
|
2366
2508
|
representative_last_name: opts.lastName ?? stored.representative_last_name,
|
|
2367
2509
|
representative_title: opts.title ?? stored.representative_title,
|
|
@@ -2522,9 +2664,11 @@ managed.command("ads-container <orgSlug>").description(
|
|
|
2522
2664
|
timeZone: opts.timeZone,
|
|
2523
2665
|
replaceCurrentAccount: opts.replaceCurrentAccount
|
|
2524
2666
|
});
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
)
|
|
2667
|
+
let outcome;
|
|
2668
|
+
if (c.already_provisioned) outcome = `Google Ads container already existed for ${c.org_slug}`;
|
|
2669
|
+
else if (c.adopted) outcome = `Adopted Google Ads account ${c.customer_id} as the container for ${c.org_slug}`;
|
|
2670
|
+
else outcome = `Created Google Ads container for ${c.org_slug}`;
|
|
2671
|
+
console.log(outcome);
|
|
2528
2672
|
printAdsContainer(c);
|
|
2529
2673
|
const leftBehind = c.previous_campaign_count ?? 0;
|
|
2530
2674
|
if (c.previous_customer_id && leftBehind > 0) {
|
|
@@ -2583,6 +2727,90 @@ This key operates every org you manage; target one with X-Organization-ID (or \`
|
|
|
2583
2727
|
fail(e);
|
|
2584
2728
|
}
|
|
2585
2729
|
});
|
|
2730
|
+
managed.command("performance").description(
|
|
2731
|
+
"Daily spend, traffic and captured leads for the client orgs you manage \u2014 one organization with --organization, otherwise every client. A metric whose source dataset is missing or unreadable prints as '-' and is explained under coverage; it is never reported as zero."
|
|
2732
|
+
).option("--organization <slug>", "read one organization instead of every client org").option("--from <YYYY-MM-DD>", "first day to include (inclusive); defaults to 89 days before --to").option("--to <YYYY-MM-DD>", "last day to include (inclusive); defaults to today (UTC)").option("--daily", "one row per organization per day instead of window totals").option("--json", "print the raw JSON result instead of a table").action(
|
|
2733
|
+
async (opts) => {
|
|
2734
|
+
try {
|
|
2735
|
+
const res = await new ErdoClient().managerPerformance({
|
|
2736
|
+
organization_slug: opts.organization,
|
|
2737
|
+
from: opts.from,
|
|
2738
|
+
to: opts.to
|
|
2739
|
+
});
|
|
2740
|
+
if (opts.json) {
|
|
2741
|
+
print(res);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (res.organizations.length === 0) {
|
|
2745
|
+
console.log(res.message || "No organizations to read.");
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
const cell = (v, digits = 0) => v === null ? "-" : v.toFixed(digits);
|
|
2749
|
+
const total = (days, pick) => {
|
|
2750
|
+
let sum = 0;
|
|
2751
|
+
for (const day of days) {
|
|
2752
|
+
const v = pick(day);
|
|
2753
|
+
if (v === null) return null;
|
|
2754
|
+
sum += v;
|
|
2755
|
+
}
|
|
2756
|
+
return sum;
|
|
2757
|
+
};
|
|
2758
|
+
if (opts.daily) {
|
|
2759
|
+
const rows = [];
|
|
2760
|
+
for (const org2 of res.organizations) {
|
|
2761
|
+
for (const day of org2.days) {
|
|
2762
|
+
rows.push([
|
|
2763
|
+
org2.organization_slug,
|
|
2764
|
+
day.date,
|
|
2765
|
+
cell(day.spend, 2),
|
|
2766
|
+
cell(day.clicks),
|
|
2767
|
+
cell(day.visits),
|
|
2768
|
+
cell(day.form_starts),
|
|
2769
|
+
cell(day.leads_on_page),
|
|
2770
|
+
cell(day.leads_captured)
|
|
2771
|
+
]);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
printAlignedTable(
|
|
2775
|
+
["organization", "date", "spend", "clicks", "visits", "form starts", "page leads", "leads"],
|
|
2776
|
+
rows
|
|
2777
|
+
);
|
|
2778
|
+
} else {
|
|
2779
|
+
printAlignedTable(
|
|
2780
|
+
["organization", "spend", "clicks", "visits", "form starts", "page leads", "leads", "cost/lead"],
|
|
2781
|
+
res.organizations.map((org2) => {
|
|
2782
|
+
const spend = total(org2.days, (d) => d.spend);
|
|
2783
|
+
const leads = total(org2.days, (d) => d.leads_captured);
|
|
2784
|
+
const costPerLead = spend === null || leads === null || leads === 0 ? null : spend / leads;
|
|
2785
|
+
return [
|
|
2786
|
+
org2.organization_slug,
|
|
2787
|
+
cell(spend, 2),
|
|
2788
|
+
cell(total(org2.days, (d) => d.clicks)),
|
|
2789
|
+
cell(total(org2.days, (d) => d.visits)),
|
|
2790
|
+
cell(total(org2.days, (d) => d.form_starts)),
|
|
2791
|
+
cell(total(org2.days, (d) => d.leads_on_page)),
|
|
2792
|
+
cell(leads, 0),
|
|
2793
|
+
cell(costPerLead, 2)
|
|
2794
|
+
];
|
|
2795
|
+
})
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
process.stderr.write(`
|
|
2799
|
+
${res.from} to ${res.to} (${res.timezone} days)
|
|
2800
|
+
`);
|
|
2801
|
+
for (const org2 of res.organizations) {
|
|
2802
|
+
for (const gap of org2.coverage) {
|
|
2803
|
+
process.stderr.write(
|
|
2804
|
+
`${org2.organization_slug}: ${gap.metrics.join(", ")} unavailable \u2014 ${gap.reason}
|
|
2805
|
+
`
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
} catch (e) {
|
|
2810
|
+
fail(e);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
);
|
|
2586
2814
|
var tokenCmd = program.command("token").description("Manage your API tokens (account-level credentials)");
|
|
2587
2815
|
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(
|
|
2588
2816
|
orgOption("the token's default org (defaults to your active org; must be one you belong to)")
|
|
@@ -2871,7 +3099,7 @@ function splitVariantSpec(spec) {
|
|
|
2871
3099
|
const key = eq > 0 ? trimmed.slice(0, eq).trim() : "";
|
|
2872
3100
|
const startsToken = trimmed === "control" || trimmed === "is_control" || key !== "" && VARIANT_SPEC_KEYS.has(key);
|
|
2873
3101
|
if (!startsToken && parts.length > 0 && COMMA_SAFE_VARIANT_KEYS.has(lastKey)) {
|
|
2874
|
-
parts[parts.length - 1] +=
|
|
3102
|
+
parts[parts.length - 1] += `,${raw}`;
|
|
2875
3103
|
continue;
|
|
2876
3104
|
}
|
|
2877
3105
|
parts.push(raw);
|
|
@@ -3381,9 +3609,13 @@ ${"Case".padEnd(34)}${res.models.map(modelCol).join("")}`);
|
|
|
3381
3609
|
console.log(`
|
|
3382
3610
|
${"Summary (avg of all cases)".padEnd(34)}${res.models.map(modelCol).join("")}`);
|
|
3383
3611
|
for (const stat of ["score", "pass", "cost", "secs"]) {
|
|
3384
|
-
let line =
|
|
3612
|
+
let line = ` ${stat}`.padEnd(34);
|
|
3385
3613
|
for (const s of res.summary) {
|
|
3386
|
-
|
|
3614
|
+
let v;
|
|
3615
|
+
if (stat === "score") v = s.avg_score.toFixed(2);
|
|
3616
|
+
else if (stat === "pass") v = `${Math.round(s.pass_rate * 100)}%`;
|
|
3617
|
+
else if (stat === "cost") v = `$${(s.avg_cost_millicents / 1e5).toFixed(3)}/case`;
|
|
3618
|
+
else v = `${(s.avg_duration_ms / 1e3).toFixed(0)}s`;
|
|
3387
3619
|
line += v.padEnd(18);
|
|
3388
3620
|
}
|
|
3389
3621
|
console.log(line);
|
|
@@ -3731,9 +3963,11 @@ function printRunDetail(detail, wantResources, wantSteps) {
|
|
|
3731
3963
|
group.set(key, entry);
|
|
3732
3964
|
byKind.set(kind, group);
|
|
3733
3965
|
}
|
|
3734
|
-
const kinds = [...byKind.keys()].sort(
|
|
3735
|
-
(a
|
|
3736
|
-
|
|
3966
|
+
const kinds = [...byKind.keys()].sort((a, b) => {
|
|
3967
|
+
if (a === "skill") return -1;
|
|
3968
|
+
if (b === "skill") return 1;
|
|
3969
|
+
return a.localeCompare(b);
|
|
3970
|
+
});
|
|
3737
3971
|
for (const kind of kinds) {
|
|
3738
3972
|
console.log(` ${kind}`);
|
|
3739
3973
|
for (const entry of byKind.get(kind).values()) {
|
|
@@ -3811,6 +4045,46 @@ function decisionLine(d) {
|
|
|
3811
4045
|
}
|
|
3812
4046
|
return parts.join(" ");
|
|
3813
4047
|
}
|
|
4048
|
+
function renderApprovalCard(approval) {
|
|
4049
|
+
console.log("");
|
|
4050
|
+
console.log("Approval:");
|
|
4051
|
+
const headline = approval.action_display || approval.action_headline;
|
|
4052
|
+
if (headline) console.log(` ${headline}`);
|
|
4053
|
+
if (approval.action_context) console.log(` ${approval.action_context}`);
|
|
4054
|
+
for (const item of approval.items ?? []) {
|
|
4055
|
+
const params = item.params ? Object.entries(item.params).map(([k, v]) => `${k}=${v}`).join(", ") : "";
|
|
4056
|
+
console.log(` - ${item.what}${params ? ` (${params})` : ""}`);
|
|
4057
|
+
if (item.why) console.log(` why: ${item.why}`);
|
|
4058
|
+
}
|
|
4059
|
+
if (approval.omitted_items) console.log(` and ${approval.omitted_items} more`);
|
|
4060
|
+
if (approval.occurrence_count && approval.occurrence_count > 1) {
|
|
4061
|
+
console.log(` proposed ${approval.occurrence_count}x`);
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
function renderReportEffect(e) {
|
|
4065
|
+
if (e.measurability !== "measurable") {
|
|
4066
|
+
const reason = e.unmeasurable_reason ? `: ${e.unmeasurable_reason}` : "";
|
|
4067
|
+
console.log(` ${e.metric || e.measurability} \u2014 ${e.measurability}${reason}`);
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
const predicateValue = e.target_value ?? e.min_delta;
|
|
4071
|
+
const predicate = [
|
|
4072
|
+
e.metric,
|
|
4073
|
+
e.success_operator,
|
|
4074
|
+
predicateValue !== void 0 && predicateValue !== null ? formatNumber(predicateValue) : void 0
|
|
4075
|
+
].filter((p) => p !== void 0 && p !== null && p !== "").join(" ");
|
|
4076
|
+
const windows = [];
|
|
4077
|
+
if (e.baseline_window_days) windows.push(`baseline ${e.baseline_window_days}d`);
|
|
4078
|
+
if (e.outcome_window_days) windows.push(`outcome ${e.outcome_window_days}d`);
|
|
4079
|
+
const windowStr = windows.length ? ` (${windows.join(", ")})` : "";
|
|
4080
|
+
console.log(` ${predicate || e.metric || "effect"}${windowStr} \u2014 ${e.status}`);
|
|
4081
|
+
if (e.baseline_value !== void 0 && e.baseline_value !== null && e.outcome_value !== void 0 && e.outcome_value !== null) {
|
|
4082
|
+
const label = e.outcome_label ? ` (${e.outcome_label})` : "";
|
|
4083
|
+
console.log(` measured: ${formatNumber(e.baseline_value)} \u2192 ${formatNumber(e.outcome_value)}${label}`);
|
|
4084
|
+
} else if (e.outcome_label) {
|
|
4085
|
+
console.log(` outcome: ${e.outcome_label}`);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
3814
4088
|
decisionsCmd.command("list").description("List decisions, newest first").option("--workstream <slug>", "only decisions filed under this workstream/Strategy").option(
|
|
3815
4089
|
"--source <source>",
|
|
3816
4090
|
"producer family: approval | escalation | engine_gate | allocator | experiment | workstream_commitment"
|
|
@@ -3870,6 +4144,7 @@ function renderDecision(detail) {
|
|
|
3870
4144
|
console.log("");
|
|
3871
4145
|
console.log(`Why: ${d.why}`);
|
|
3872
4146
|
}
|
|
4147
|
+
if (d.alternatives_considered) console.log(`Alternatives considered: ${d.alternatives_considered}`);
|
|
3873
4148
|
if (d.decider_rationale) console.log(`Decider said: ${d.decider_rationale}`);
|
|
3874
4149
|
if (detail.actions.length) {
|
|
3875
4150
|
console.log("");
|
|
@@ -3879,6 +4154,12 @@ function renderDecision(detail) {
|
|
|
3879
4154
|
if (a.subject_label) console.log(` subject: ${a.subject_label}`);
|
|
3880
4155
|
if (a.effective_at) console.log(` effective: ${a.effective_at}`);
|
|
3881
4156
|
if (a.result_summary) console.log(` result: ${a.result_summary}`);
|
|
4157
|
+
if (a.annotations && Object.keys(a.annotations).length) {
|
|
4158
|
+
console.log(" before:");
|
|
4159
|
+
for (const [k, v] of Object.entries(a.annotations)) {
|
|
4160
|
+
console.log(` ${k}: ${compactJSON(v) ?? "null"}`);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
3882
4163
|
});
|
|
3883
4164
|
}
|
|
3884
4165
|
if (detail.effects.length) {
|
|
@@ -3896,13 +4177,28 @@ function renderDecision(detail) {
|
|
|
3896
4177
|
if (e.unmeasurable_reason) console.log(` reason: ${e.unmeasurable_reason}`);
|
|
3897
4178
|
});
|
|
3898
4179
|
}
|
|
3899
|
-
|
|
4180
|
+
const lineage = detail.lineage;
|
|
4181
|
+
if (lineage.supersedes_slug || lineage.superseded_by_slugs?.length || lineage.workstream_slug || lineage.attention_item_slug) {
|
|
3900
4182
|
console.log("");
|
|
3901
4183
|
console.log("Lineage:");
|
|
3902
|
-
if (
|
|
3903
|
-
for (const slug of
|
|
4184
|
+
if (lineage.supersedes_slug) console.log(` replaced ${lineage.supersedes_slug}`);
|
|
4185
|
+
for (const slug of lineage.superseded_by_slugs ?? []) {
|
|
3904
4186
|
console.log(` replaced by ${slug}`);
|
|
3905
4187
|
}
|
|
4188
|
+
if (lineage.workstream_slug) {
|
|
4189
|
+
console.log(` workstream: ${lineage.workstream_title || lineage.workstream_slug} (${lineage.workstream_slug})`);
|
|
4190
|
+
}
|
|
4191
|
+
if (lineage.attention_item_slug) {
|
|
4192
|
+
console.log(
|
|
4193
|
+
` raised by: ${lineage.attention_item_title || lineage.attention_item_slug} (${lineage.attention_item_slug})`
|
|
4194
|
+
);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
if (lineage.approval) renderApprovalCard(lineage.approval);
|
|
4198
|
+
else if (lineage.approval_unreadable) {
|
|
4199
|
+
console.log("");
|
|
4200
|
+
console.log("Approval:");
|
|
4201
|
+
console.log(" The approval card could not be loaded.");
|
|
3906
4202
|
}
|
|
3907
4203
|
}
|
|
3908
4204
|
decisionsCmd.command("show <slug>").description(
|
|
@@ -4176,9 +4472,7 @@ approvalsCmd.command("settings [mode]").description(
|
|
|
4176
4472
|
try {
|
|
4177
4473
|
const client = new ErdoClient();
|
|
4178
4474
|
let gates;
|
|
4179
|
-
if (opts.gates
|
|
4180
|
-
gates = opts.gates.split(",").map((g) => g.trim()).filter(Boolean);
|
|
4181
|
-
} else {
|
|
4475
|
+
if (opts.gates === void 0) {
|
|
4182
4476
|
switch (mode) {
|
|
4183
4477
|
case void 0:
|
|
4184
4478
|
break;
|
|
@@ -4195,6 +4489,8 @@ approvalsCmd.command("settings [mode]").description(
|
|
|
4195
4489
|
default:
|
|
4196
4490
|
throw new Error(`unknown mode ${mode} \u2014 use safe | all | reset, or --gates spend,destructive`);
|
|
4197
4491
|
}
|
|
4492
|
+
} else {
|
|
4493
|
+
gates = opts.gates.split(",").map((g) => g.trim()).filter(Boolean);
|
|
4198
4494
|
}
|
|
4199
4495
|
if (gates !== void 0) {
|
|
4200
4496
|
await client.setApprovalSettings(gates);
|
|
@@ -4212,19 +4508,100 @@ approvalsCmd.command("settings [mode]").description(
|
|
|
4212
4508
|
}
|
|
4213
4509
|
});
|
|
4214
4510
|
var attnCmd = program.command("attention").description("The attention feed \u2014 digests, choices, escalations awaiting a human");
|
|
4215
|
-
|
|
4511
|
+
function attentionItemLine(item) {
|
|
4512
|
+
return [item.slug, item.severity, item.kind, item.status, item.title].join(" ");
|
|
4513
|
+
}
|
|
4514
|
+
function renderAttentionItemReport(item, report) {
|
|
4515
|
+
console.log("");
|
|
4516
|
+
console.log("Safe default:");
|
|
4517
|
+
const expires = formatDate(item.expires_at);
|
|
4518
|
+
if (report.proposed_answer) {
|
|
4519
|
+
if (!expires) {
|
|
4520
|
+
console.log(` Erdo proposes: ${report.proposed_answer}`);
|
|
4521
|
+
console.log(" No deadline \u2014 nothing applies until somebody answers");
|
|
4522
|
+
} else if (report.safe_default_approves) {
|
|
4523
|
+
console.log(` If nobody answers: ${report.proposed_answer}`);
|
|
4524
|
+
console.log(` Applies ${expires}`);
|
|
4525
|
+
} else {
|
|
4526
|
+
console.log(` Erdo proposes: ${report.proposed_answer}`);
|
|
4527
|
+
console.log(` If nobody answers, Erdo will NOT do this \u2014 the item closes ${expires}`);
|
|
4528
|
+
}
|
|
4529
|
+
} else {
|
|
4530
|
+
console.log(expires ? ` Closes unanswered ${expires}` : " Does not expire");
|
|
4531
|
+
}
|
|
4532
|
+
if (report.has_pending_action) console.log(" Holds a pending engine action");
|
|
4533
|
+
const safeDefault = compactJSON(report.safe_default);
|
|
4534
|
+
if (safeDefault) console.log(` Default answer: ${safeDefault}`);
|
|
4535
|
+
if (report.workstream_slug || report.workstream_title) {
|
|
4536
|
+
console.log("");
|
|
4537
|
+
console.log("Workstream:");
|
|
4538
|
+
console.log(` ${report.workstream_title || report.workstream_slug} (${report.workstream_slug})`);
|
|
4539
|
+
}
|
|
4540
|
+
if (report.decision_slug) {
|
|
4541
|
+
console.log("");
|
|
4542
|
+
console.log("Decision:");
|
|
4543
|
+
if (report.decision) {
|
|
4544
|
+
const d = report.decision;
|
|
4545
|
+
console.log(` ${d.slug} \u2014 ${d.what}`);
|
|
4546
|
+
if (d.why) console.log(` why: ${d.why}`);
|
|
4547
|
+
if (d.alternatives_considered) console.log(` alternatives considered: ${d.alternatives_considered}`);
|
|
4548
|
+
console.log(` decided by: ${d.decider_kind}`);
|
|
4549
|
+
console.log(` status: ${d.status}`);
|
|
4550
|
+
if (d.outcome_label) console.log(` outcome: ${d.outcome_label}`);
|
|
4551
|
+
} else {
|
|
4552
|
+
console.log(` ${report.decision_slug} \u2014 exists, but in a project you cannot view`);
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
if (report.approval) renderApprovalCard(report.approval);
|
|
4556
|
+
else if (report.approval_unreadable) {
|
|
4557
|
+
console.log("");
|
|
4558
|
+
console.log("Approval:");
|
|
4559
|
+
console.log(" The approval card could not be loaded.");
|
|
4560
|
+
}
|
|
4561
|
+
if (report.effects?.length) {
|
|
4562
|
+
console.log("");
|
|
4563
|
+
console.log("Predicted vs measured:");
|
|
4564
|
+
for (const e of report.effects) renderReportEffect(e);
|
|
4565
|
+
}
|
|
4566
|
+
if (report.related_items?.length) {
|
|
4567
|
+
console.log("");
|
|
4568
|
+
console.log("Earlier items:");
|
|
4569
|
+
for (const r of report.related_items) {
|
|
4570
|
+
const reasons = [r.same_workstream ? "same workstream" : void 0, r.same_title ? "same title" : void 0].filter(Boolean).join(", ");
|
|
4571
|
+
const created = formatDate(r.created_at) ?? r.created_at;
|
|
4572
|
+
console.log(
|
|
4573
|
+
` [${r.severity}] ${r.status} \u2014 ${r.title} (${r.slug}) \u2014 ${created}${reasons ? ` \u2014 ${reasons}` : ""}`
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
attnCmd.command("list").description("List attention items").option("--status <status...>", "open | answered | dismissed | expired").option("--open", "shorthand for --status open").option("--engine-actions", "only engine-generated items").option("--item <idOrSlug>", "read one item: its slug or its uuid").option("--org <slug>", "read another organization's feed \u2014 yours, or a client you manage").option(
|
|
4579
|
+
"--subject-kind <kind>",
|
|
4580
|
+
"only items about this kind of thing: google_ads_campaign | meta_ads_campaign | google_ads_ad_group | google_ads_ad | page | dataset | workstream"
|
|
4581
|
+
).option("--subject-id <id>", "with --subject-kind, only items about this exact subject").option("-n, --limit <n>", "max items", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).option("--json", "output raw JSON").action(
|
|
4216
4582
|
async (opts) => {
|
|
4217
4583
|
try {
|
|
4218
4584
|
const statuses = opts.status?.length ? opts.status : opts.open ? ["open"] : void 0;
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4585
|
+
const res = await new ErdoClient().listAttentionItems({
|
|
4586
|
+
statuses,
|
|
4587
|
+
engine_actions_only: opts.engineActions,
|
|
4588
|
+
item_id: opts.item,
|
|
4589
|
+
organization_slug: opts.org,
|
|
4590
|
+
subject_kind: opts.subjectKind,
|
|
4591
|
+
subject_id: opts.subjectId,
|
|
4592
|
+
limit: opts.limit,
|
|
4593
|
+
offset: opts.offset
|
|
4594
|
+
});
|
|
4595
|
+
if (opts.json) {
|
|
4596
|
+
print(res);
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4599
|
+
if (!res.items.length) {
|
|
4600
|
+
console.log("No attention items match. Widen the filters.");
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
for (const item of res.items) console.log(attentionItemLine(item));
|
|
4604
|
+
if (res.report) renderAttentionItemReport(res.items[0], res.report);
|
|
4228
4605
|
} catch (e) {
|
|
4229
4606
|
fail(e);
|
|
4230
4607
|
}
|
|
@@ -4235,7 +4612,7 @@ attnCmd.command("respond <id>").description(
|
|
|
4235
4612
|
).option("--answer <json>", `a JSON answer, e.g. '{"q1":["option-a"]}'`).option(
|
|
4236
4613
|
"--flag-broken <json>",
|
|
4237
4614
|
'flag choice variants as broken: JSON array [{"variant_key":"...","reason":"..."}]'
|
|
4238
|
-
).option("--ack", "mark read / acknowledge").option("--dismiss", "dismiss the item").action(
|
|
4615
|
+
).option("--ack", "mark read / acknowledge").option("--dismiss", "dismiss the item").option("--org <slug>", "the organization holding the item \u2014 yours, or a client you manage").action(
|
|
4239
4616
|
async (id, opts) => {
|
|
4240
4617
|
try {
|
|
4241
4618
|
if (opts.ack || opts.dismiss) {
|
|
@@ -4247,7 +4624,8 @@ attnCmd.command("respond <id>").description(
|
|
|
4247
4624
|
}
|
|
4248
4625
|
print(
|
|
4249
4626
|
await new ErdoClient().respondAttentionItem(id, {
|
|
4250
|
-
action: opts.ack ? "acknowledge" : "dismiss"
|
|
4627
|
+
action: opts.ack ? "acknowledge" : "dismiss",
|
|
4628
|
+
organization_slug: opts.org
|
|
4251
4629
|
})
|
|
4252
4630
|
);
|
|
4253
4631
|
return;
|
|
@@ -4255,7 +4633,7 @@ attnCmd.command("respond <id>").description(
|
|
|
4255
4633
|
if (opts.answer === void 0 && opts.flagBroken === void 0) {
|
|
4256
4634
|
fail(new Error("provide one of --answer, --flag-broken, --ack, or --dismiss"));
|
|
4257
4635
|
}
|
|
4258
|
-
const input = { action: "answer" };
|
|
4636
|
+
const input = { action: "answer", organization_slug: opts.org };
|
|
4259
4637
|
if (opts.answer !== void 0) {
|
|
4260
4638
|
try {
|
|
4261
4639
|
input.answer = JSON.parse(opts.answer);
|
|
@@ -4311,7 +4689,19 @@ pageFeedbackCmd.command("record").description(
|
|
|
4311
4689
|
);
|
|
4312
4690
|
}
|
|
4313
4691
|
let items;
|
|
4314
|
-
if (opts.items
|
|
4692
|
+
if (opts.items === void 0) {
|
|
4693
|
+
if (!opts.experiment || !opts.variant || !opts.feedback) {
|
|
4694
|
+
throw new Error("provide --items, or all of --experiment, --variant, --feedback");
|
|
4695
|
+
}
|
|
4696
|
+
items = [
|
|
4697
|
+
{
|
|
4698
|
+
experiment_slug: opts.experiment,
|
|
4699
|
+
variant_key: opts.variant,
|
|
4700
|
+
feedback: opts.feedback,
|
|
4701
|
+
image_bucket_key: opts.imageKey
|
|
4702
|
+
}
|
|
4703
|
+
];
|
|
4704
|
+
} else {
|
|
4315
4705
|
let parsed;
|
|
4316
4706
|
try {
|
|
4317
4707
|
parsed = JSON.parse(readMaybeFile(opts.items));
|
|
@@ -4324,18 +4714,6 @@ pageFeedbackCmd.command("record").description(
|
|
|
4324
4714
|
throw new Error("--items must be a JSON array of {experiment_slug, variant_key, feedback} objects");
|
|
4325
4715
|
}
|
|
4326
4716
|
items = parsed;
|
|
4327
|
-
} else {
|
|
4328
|
-
if (!opts.experiment || !opts.variant || !opts.feedback) {
|
|
4329
|
-
throw new Error("provide --items, or all of --experiment, --variant, --feedback");
|
|
4330
|
-
}
|
|
4331
|
-
items = [
|
|
4332
|
-
{
|
|
4333
|
-
experiment_slug: opts.experiment,
|
|
4334
|
-
variant_key: opts.variant,
|
|
4335
|
-
feedback: opts.feedback,
|
|
4336
|
-
image_bucket_key: opts.imageKey
|
|
4337
|
-
}
|
|
4338
|
-
];
|
|
4339
4717
|
}
|
|
4340
4718
|
print(await new ErdoClient().recordPageFeedback(items));
|
|
4341
4719
|
} catch (e) {
|
|
@@ -4581,7 +4959,7 @@ reviewsCmd.command("decide <id>").description(
|
|
|
4581
4959
|
opts.apply ? "apply" : null,
|
|
4582
4960
|
opts.resolve ? "resolve" : null,
|
|
4583
4961
|
opts.reject ? "reject" : null,
|
|
4584
|
-
opts.snooze
|
|
4962
|
+
opts.snooze === void 0 ? null : "snooze"
|
|
4585
4963
|
].filter(Boolean);
|
|
4586
4964
|
if (chosen.length !== 1) {
|
|
4587
4965
|
fail(new Error("provide exactly one of --apply, --resolve, --reject, or --snooze"));
|
|
@@ -4601,6 +4979,11 @@ reviewsCmd.command("decide <id>").description(
|
|
|
4601
4979
|
}
|
|
4602
4980
|
);
|
|
4603
4981
|
var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
|
|
4982
|
+
function publicFlag(opts) {
|
|
4983
|
+
if (opts.public) return true;
|
|
4984
|
+
if (opts.private) return false;
|
|
4985
|
+
return void 0;
|
|
4986
|
+
}
|
|
4604
4987
|
function grantList(v) {
|
|
4605
4988
|
if (v === void 0) return void 0;
|
|
4606
4989
|
if (v.trim() === "[]") return [];
|
|
@@ -4676,7 +5059,9 @@ pagesCmd.command("update <id>").description(
|
|
|
4676
5059
|
writable_dataset_slugs: grantList(opts.writableDatasets),
|
|
4677
5060
|
kv_slugs: grantList(opts.kv),
|
|
4678
5061
|
writable_kv_slugs: grantList(opts.writableKv),
|
|
4679
|
-
|
|
5062
|
+
// --public and --private are mutually exclusive flags; neither set
|
|
5063
|
+
// leaves the server's default standing.
|
|
5064
|
+
public: publicFlag(opts),
|
|
4680
5065
|
request_review: !!opts.requestReview
|
|
4681
5066
|
});
|
|
4682
5067
|
console.log(`${res.id} ${res.public_url || res.url}`);
|
|
@@ -5041,11 +5426,13 @@ sendingDomainsCmd.command("remove <domain>").description(
|
|
|
5041
5426
|
});
|
|
5042
5427
|
emailCmd.command("received").description(
|
|
5043
5428
|
"Read the mail that arrived at the org's sending domain, newest first \u2014 usually leads replying to outreach an agent sent. Omit --domain: an org has one sending domain and the read resolves it."
|
|
5044
|
-
).option("--domain <domain>", "the sending domain to read (only needed if the org has more than one)").option("--limit <n>", "maximum messages to return, newest first").option("--json", "print the raw JSON result instead of the message list").action(async (opts) => {
|
|
5429
|
+
).option("--domain <domain>", "the sending domain to read (only needed if the org has more than one)").option("--limit <n>", "maximum messages to return, newest first").option("--lead-ref <ref>", "only one lead's email conversation with the desk").option("--dataset <slug>", "with --lead-ref, only that lead dataset").option("--json", "print the raw JSON result instead of the message list").action(async (opts) => {
|
|
5045
5430
|
try {
|
|
5046
5431
|
const res = await new ErdoClient().listReceivedEmails({
|
|
5047
5432
|
domain: opts.domain,
|
|
5048
|
-
limit: opts.limit ? Number(opts.limit) : void 0
|
|
5433
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
5434
|
+
leadRef: opts.leadRef,
|
|
5435
|
+
dataset: opts.dataset
|
|
5049
5436
|
});
|
|
5050
5437
|
if (opts.json) {
|
|
5051
5438
|
print(res);
|
|
@@ -5062,12 +5449,17 @@ emailCmd.command("received").description(
|
|
|
5062
5449
|
`);
|
|
5063
5450
|
for (const m of emails) {
|
|
5064
5451
|
const who = m.from_name ? `${m.from_name} <${m.from_email}>` : m.from_email;
|
|
5065
|
-
|
|
5452
|
+
const side = m.direction ? ` [${m.direction}${m.lead_email ? ` \xB7 lead ${m.lead_email}` : ""}]` : "";
|
|
5453
|
+
console.log(`${m.received_at} ${who} ${m.subject || "(no subject)"}${side}`);
|
|
5066
5454
|
const preview = (m.text_preview || "").replace(/\s+/g, " ").trim();
|
|
5067
5455
|
if (preview) console.log(` ${preview.length > 160 ? `${preview.slice(0, 160)}\u2026` : preview}`);
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5456
|
+
if (m.relay_skip_reason) {
|
|
5457
|
+
console.log(` kept in Erdo, not relayed (${m.relay_skip_reason})`);
|
|
5458
|
+
} else {
|
|
5459
|
+
console.log(
|
|
5460
|
+
m.forwarded_at ? ` ${m.relayed_email_id ? "relayed" : "forwarded"} ${m.forwarded_at}` : " not forwarded \u2014 this one is in Erdo only, so nobody on your side has necessarily seen it"
|
|
5461
|
+
);
|
|
5462
|
+
}
|
|
5071
5463
|
}
|
|
5072
5464
|
} catch (e) {
|
|
5073
5465
|
fail(e);
|
|
@@ -5265,6 +5657,8 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5265
5657
|
"status",
|
|
5266
5658
|
"secs",
|
|
5267
5659
|
"transcript",
|
|
5660
|
+
"placed by",
|
|
5661
|
+
"dial",
|
|
5268
5662
|
"contact",
|
|
5269
5663
|
"lead",
|
|
5270
5664
|
"lead ref",
|
|
@@ -5279,6 +5673,10 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5279
5673
|
call.status,
|
|
5280
5674
|
call.duration_seconds,
|
|
5281
5675
|
call.has_transcript ? "yes" : "no",
|
|
5676
|
+
// "-" rather than blank: a call the AI agent placed has no person
|
|
5677
|
+
// behind it, which is a fact, not a missing value.
|
|
5678
|
+
call.placed_by_name || "-",
|
|
5679
|
+
call.dial_outcome || "-",
|
|
5282
5680
|
contactLabel(call.contact),
|
|
5283
5681
|
leadStatusLabel(call.lead_status),
|
|
5284
5682
|
leadReferenceLabel(call.lead_reference),
|
|
@@ -5298,7 +5696,7 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5298
5696
|
}
|
|
5299
5697
|
);
|
|
5300
5698
|
voiceCallsCmd.command("get <callID>").description(
|
|
5301
|
-
"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)"
|
|
5699
|
+
"Read one phone call in full: transcript, summary, per-turn LLM metrics, who placed it by hand (placed_by_name) and what became of the lead's leg (dial_outcome), and the lead it produced (contact, lead_status, lead_saved_at, canonical_lead_id, lead_reference)"
|
|
5302
5700
|
).action(async (callID) => {
|
|
5303
5701
|
try {
|
|
5304
5702
|
print(await new ErdoClient().getVoiceCall(callID));
|
|
@@ -5537,12 +5935,14 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
5537
5935
|
if (numbers.length === 0) {
|
|
5538
5936
|
console.log("This organization holds no phone numbers.");
|
|
5539
5937
|
} else {
|
|
5938
|
+
const callable = new Set(res.person_call_numbers ?? []);
|
|
5540
5939
|
printAlignedTable(
|
|
5541
|
-
["number", "agent", "kind", "status", "waiting on", "reason"],
|
|
5940
|
+
["number", "agent", "kind", "calls", "status", "waiting on", "reason"],
|
|
5542
5941
|
numbers.map((n) => [
|
|
5543
5942
|
n.address,
|
|
5544
5943
|
n.agent_ref ?? "",
|
|
5545
5944
|
n.sender_kind,
|
|
5945
|
+
callable.has(n.address) ? "yes" : "",
|
|
5546
5946
|
n.status,
|
|
5547
5947
|
n.next_step ?? "",
|
|
5548
5948
|
n.reason ?? ""
|
|
@@ -5608,7 +6008,27 @@ voiceNumbersCmd.command("provision-sms <number>").description("Ask for A2P SMS r
|
|
|
5608
6008
|
fail(e);
|
|
5609
6009
|
}
|
|
5610
6010
|
});
|
|
5611
|
-
var voiceAgentsCmd = voiceCmd.command("agents").description("
|
|
6011
|
+
var voiceAgentsCmd = voiceCmd.command("agents").description("List the organization's concierges and change settings on one");
|
|
6012
|
+
voiceAgentsCmd.command("list", { isDefault: true }).description(
|
|
6013
|
+
"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."
|
|
6014
|
+
).option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
|
|
6015
|
+
try {
|
|
6016
|
+
const res = await new ErdoClient().listVoiceAgents();
|
|
6017
|
+
if (opts.json) {
|
|
6018
|
+
print(res);
|
|
6019
|
+
return;
|
|
6020
|
+
}
|
|
6021
|
+
if (!res.agents.length) {
|
|
6022
|
+
console.error("(no concierges in this organization)");
|
|
6023
|
+
return;
|
|
6024
|
+
}
|
|
6025
|
+
for (const a of res.agents) {
|
|
6026
|
+
console.log(`${a.id} ${a.slug} ${a.phone_number || "-"} ${a.name}`);
|
|
6027
|
+
}
|
|
6028
|
+
} catch (e) {
|
|
6029
|
+
fail(e);
|
|
6030
|
+
}
|
|
6031
|
+
});
|
|
5612
6032
|
voiceAgentsCmd.command("sms-replies <slug>").description("Turn a voice agent's answering of texts on or off").option("--on", "the agent answers texts sent to its own number").option("--off", "texts still arrive and stay readable; the agent does not write back").action(async (slug, opts) => {
|
|
5613
6033
|
if (Boolean(opts.on) === Boolean(opts.off)) {
|
|
5614
6034
|
fail(new Error("say which way: pass --on or --off (exactly one)"));
|
|
@@ -5627,6 +6047,28 @@ voiceAgentsCmd.command("sms-replies <slug>").description("Turn a voice agent's a
|
|
|
5627
6047
|
fail(e);
|
|
5628
6048
|
}
|
|
5629
6049
|
});
|
|
6050
|
+
voiceAgentsCmd.command("whatsapp-replies <slug>").description("Turn a voice agent's answering on WhatsApp on or off").option("--on", "the agent answers WhatsApp messages sent to its own number").option("--off", "messages still arrive and stay readable; the agent does not write back").action(async (slug, opts) => {
|
|
6051
|
+
if (Boolean(opts.on) === Boolean(opts.off)) {
|
|
6052
|
+
fail(new Error("say which way: pass --on or --off (exactly one)"));
|
|
6053
|
+
}
|
|
6054
|
+
try {
|
|
6055
|
+
const agent = await new ErdoClient().setVoiceAgentWhatsAppReplies(slug, Boolean(opts.on));
|
|
6056
|
+
console.log(
|
|
6057
|
+
`${agent.slug} ${agent.whatsapp_replies_enabled ? "on" : "off"} ${agent.phone_number ?? "-"} ${agent.name}`
|
|
6058
|
+
);
|
|
6059
|
+
if (!agent.phone_number) {
|
|
6060
|
+
process.stderr.write(
|
|
6061
|
+
"this agent has no phone number yet \u2014 the setting is stored and applies once it gets one\n"
|
|
6062
|
+
);
|
|
6063
|
+
} else {
|
|
6064
|
+
process.stderr.write(
|
|
6065
|
+
"the number also has to be set up as a WhatsApp sender before any reply can go out\n"
|
|
6066
|
+
);
|
|
6067
|
+
}
|
|
6068
|
+
} catch (e) {
|
|
6069
|
+
fail(e);
|
|
6070
|
+
}
|
|
6071
|
+
});
|
|
5630
6072
|
var datasetsCmd = program.command("datasets").description("Datasets");
|
|
5631
6073
|
datasetsCmd.command("list").description("List datasets").option(
|
|
5632
6074
|
"--class <class>",
|
|
@@ -6070,8 +6512,10 @@ refreshCmd.command("set <slug>").description(
|
|
|
6070
6512
|
const slugs = opts.contextDatasets.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6071
6513
|
if (slugs.length) body.context_dataset_slugs = slugs;
|
|
6072
6514
|
}
|
|
6073
|
-
if (
|
|
6074
|
-
if (opts.staleAfter !== void 0)
|
|
6515
|
+
if (opts.onView !== void 0) body.refresh_on_view_enabled = opts.onView;
|
|
6516
|
+
if (opts.staleAfter !== void 0) {
|
|
6517
|
+
body.refresh_on_view_stale_after_seconds = opts.staleAfter;
|
|
6518
|
+
}
|
|
6075
6519
|
if (opts.debounce !== void 0) body.refresh_on_view_debounce_seconds = opts.debounce;
|
|
6076
6520
|
if (opts.liveWriteMode) body.live_write_mode = opts.liveWriteMode;
|
|
6077
6521
|
if (opts.payloadSchema) {
|
|
@@ -6208,7 +6652,9 @@ filterCmd.command("list <slug>").description("List the default filters on a data
|
|
|
6208
6652
|
fail(e);
|
|
6209
6653
|
}
|
|
6210
6654
|
});
|
|
6211
|
-
var leadsCmd = program.command("leads").description(
|
|
6655
|
+
var leadsCmd = program.command("leads").description(
|
|
6656
|
+
"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"
|
|
6657
|
+
);
|
|
6212
6658
|
leadsCmd.command("get <dataset> <lead>").description(
|
|
6213
6659
|
"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."
|
|
6214
6660
|
).option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
|
|
@@ -6305,6 +6751,404 @@ leadsCmd.command("merge <dataset> <survivor>").description(
|
|
|
6305
6751
|
}
|
|
6306
6752
|
}
|
|
6307
6753
|
);
|
|
6754
|
+
var leadPlaybookCmd = leadsCmd.command("playbook").description("Read, try and save the lead playbook: the plain-text rules for what Erdo does next with each lead");
|
|
6755
|
+
leadPlaybookCmd.command("get").description(
|
|
6756
|
+
"Show the saved playbook: whether the sweep is enabled, the daily contact cap, the read-back Erdo enforces (each action's mode and the working hours), and the text. An organization that never saved one sees the default template."
|
|
6757
|
+
).option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
|
|
6758
|
+
try {
|
|
6759
|
+
const res = await new ErdoClient().getLeadPlaybook();
|
|
6760
|
+
if (opts.json) {
|
|
6761
|
+
print(res);
|
|
6762
|
+
return;
|
|
6763
|
+
}
|
|
6764
|
+
printLeadPlaybook(res);
|
|
6765
|
+
} catch (e) {
|
|
6766
|
+
fail(e);
|
|
6767
|
+
}
|
|
6768
|
+
});
|
|
6769
|
+
leadPlaybookCmd.command("set").description(
|
|
6770
|
+
"Save the playbook text from a file. Erdo reads it into the typed read-back and stores both; a text it cannot read is refused rather than saved with a guess. --enable turns the sweep on, which lets Erdo email leads automatically wherever the text marks email automatic. Organization admins only."
|
|
6771
|
+
).requiredOption("-f, --file <path>", "file holding the full playbook text").option("--enable", "turn the sweep on for the organization").option("--disable", "turn the sweep off for the organization").option(
|
|
6772
|
+
"--daily-contact-cap <n>",
|
|
6773
|
+
"emails sent or put in front of a person per 24 hours, 0 to 500 (default 40)",
|
|
6774
|
+
(v) => parseInt(v, 10)
|
|
6775
|
+
).option(
|
|
6776
|
+
"--agent-id <id>",
|
|
6777
|
+
"link the concierge every draft is written as, by its id from `erdo voice agents`. Unlike the other options this one is not kept: leave it off and the link is cleared"
|
|
6778
|
+
).option("--json", "print the raw JSON result instead of a summary").action(
|
|
6779
|
+
async (opts) => {
|
|
6780
|
+
try {
|
|
6781
|
+
if (opts.enable && opts.disable) {
|
|
6782
|
+
throw new Error("pass --enable or --disable, not both");
|
|
6783
|
+
}
|
|
6784
|
+
if (opts.dailyContactCap !== void 0 && !Number.isInteger(opts.dailyContactCap)) {
|
|
6785
|
+
throw new Error("--daily-contact-cap must be a whole number");
|
|
6786
|
+
}
|
|
6787
|
+
const body = readFileSync4(opts.file, "utf8");
|
|
6788
|
+
const res = await new ErdoClient().putLeadPlaybook({
|
|
6789
|
+
body,
|
|
6790
|
+
enabled: opts.enable ? true : opts.disable ? false : void 0,
|
|
6791
|
+
daily_contact_cap: opts.dailyContactCap,
|
|
6792
|
+
agent_id: opts.agentId
|
|
6793
|
+
});
|
|
6794
|
+
if (opts.json) {
|
|
6795
|
+
print(res);
|
|
6796
|
+
return;
|
|
6797
|
+
}
|
|
6798
|
+
console.log(`Saved revision ${res.revision}.`);
|
|
6799
|
+
printLeadPlaybook(res, { withBody: false });
|
|
6800
|
+
} catch (e) {
|
|
6801
|
+
fail(e);
|
|
6802
|
+
}
|
|
6803
|
+
}
|
|
6804
|
+
);
|
|
6805
|
+
leadPlaybookCmd.command("read-back").description(
|
|
6806
|
+
"Show how Erdo reads a draft playbook without saving it: the stages, each action's mode after platform limits, the limits Erdo added, and the working hours."
|
|
6807
|
+
).requiredOption("-f, --file <path>", "file holding the draft playbook text").option(
|
|
6808
|
+
"--agent-id <id>",
|
|
6809
|
+
"read the draft back as saving it with that concierge would; leave it off to read it back with none, which is what saving with none does"
|
|
6810
|
+
).option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
|
|
6811
|
+
try {
|
|
6812
|
+
const res = await new ErdoClient().readLeadPlaybook(readFileSync4(opts.file, "utf8"), opts.agentId);
|
|
6813
|
+
if (opts.json) {
|
|
6814
|
+
print(res);
|
|
6815
|
+
return;
|
|
6816
|
+
}
|
|
6817
|
+
printLeadReadBack(res);
|
|
6818
|
+
} catch (e) {
|
|
6819
|
+
fail(e);
|
|
6820
|
+
}
|
|
6821
|
+
});
|
|
6822
|
+
leadPlaybookCmd.command("revisions").description(
|
|
6823
|
+
"List every saved version of the playbook, newest first: the revision number, whether the sweep was on, the daily cap, the concierge it was saved with and who saved it when. History starts at the revision that was current when Erdo began keeping it."
|
|
6824
|
+
).option("--limit <n>", "maximum revisions to return (default 20, maximum 100)", (v) => parseInt(v, 10)).option("--offset <n>", "revisions to skip, for paging further back", (v) => parseInt(v, 10)).option("--json", "print the raw JSON result instead of a summary").action(async (opts) => {
|
|
6825
|
+
try {
|
|
6826
|
+
const res = await new ErdoClient().listLeadPlaybookRevisions({ limit: opts.limit, offset: opts.offset });
|
|
6827
|
+
if (opts.json) {
|
|
6828
|
+
print(res);
|
|
6829
|
+
return;
|
|
6830
|
+
}
|
|
6831
|
+
if (!res.revisions?.length) {
|
|
6832
|
+
console.log("No playbook revisions are kept for this organization.");
|
|
6833
|
+
return;
|
|
6834
|
+
}
|
|
6835
|
+
printAlignedTable(
|
|
6836
|
+
["revision", "saved", "enabled", "daily cap", "concierge", "saved by"],
|
|
6837
|
+
res.revisions.map((r) => [
|
|
6838
|
+
String(r.revision),
|
|
6839
|
+
r.saved_at,
|
|
6840
|
+
r.enabled ? "yes" : "no",
|
|
6841
|
+
String(r.daily_contact_cap),
|
|
6842
|
+
r.agent_id ?? "none",
|
|
6843
|
+
r.saved_by ?? ""
|
|
6844
|
+
])
|
|
6845
|
+
);
|
|
6846
|
+
console.log(`
|
|
6847
|
+
${res.revisions.length} of ${res.total} kept revision(s). Read one with \`erdo leads playbook revision <n>\`.`);
|
|
6848
|
+
} catch (e) {
|
|
6849
|
+
fail(e);
|
|
6850
|
+
}
|
|
6851
|
+
});
|
|
6852
|
+
leadPlaybookCmd.command("revision <revision>").description(
|
|
6853
|
+
"Show one saved revision in full: the text exactly as it was saved and the read-back exactly as Erdo understood it then. This is what explains a decision whose playbook_revision names it."
|
|
6854
|
+
).option("--json", "print the raw JSON result instead of a summary").action(async (revision, opts) => {
|
|
6855
|
+
try {
|
|
6856
|
+
const n = parseInt(revision, 10);
|
|
6857
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
6858
|
+
fail(new Error("revision must be a positive number"));
|
|
6859
|
+
return;
|
|
6860
|
+
}
|
|
6861
|
+
const res = await new ErdoClient().getLeadPlaybookRevision(n);
|
|
6862
|
+
if (opts.json) {
|
|
6863
|
+
print(res);
|
|
6864
|
+
return;
|
|
6865
|
+
}
|
|
6866
|
+
printLeadPlaybookRevision(res);
|
|
6867
|
+
} catch (e) {
|
|
6868
|
+
fail(e);
|
|
6869
|
+
}
|
|
6870
|
+
});
|
|
6871
|
+
leadsCmd.command("timeline <dataset> <lead>").description(
|
|
6872
|
+
"Show everything the next-action evaluation reads about one lead: captures, sent emails, replies, SMS, calls, website chats, bookings and earlier suggestions in time order, plus the timing facts a follow-up rule matches on. <lead> is the canonical_lead_id UUID or the 22-character lead reference."
|
|
6873
|
+
).option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
|
|
6874
|
+
try {
|
|
6875
|
+
const res = await new ErdoClient().getLeadTimeline(dataset, lead);
|
|
6876
|
+
if (opts.json) {
|
|
6877
|
+
print(res);
|
|
6878
|
+
return;
|
|
6879
|
+
}
|
|
6880
|
+
console.log(`lead ${res.canonical_lead_id}`);
|
|
6881
|
+
console.log(`reference ${res.lead_reference}`);
|
|
6882
|
+
console.log(`dataset ${res.dataset_slug}`);
|
|
6883
|
+
if (res.entries?.length) {
|
|
6884
|
+
console.log("\nentries");
|
|
6885
|
+
printAlignedTable(
|
|
6886
|
+
["when", "kind", "direction", "status", "summary"],
|
|
6887
|
+
res.entries.map((e) => [e.at, e.kind, e.direction ?? "", e.status ?? "", clipLeadText(e.summary, 90)])
|
|
6888
|
+
);
|
|
6889
|
+
} else {
|
|
6890
|
+
console.log("\nNo timeline entries.");
|
|
6891
|
+
}
|
|
6892
|
+
console.log("\ntiming");
|
|
6893
|
+
printAlignedTable(
|
|
6894
|
+
["fact", "value"],
|
|
6895
|
+
Object.entries(res.timing ?? {}).map(([k, v]) => [k, v === null || v === void 0 ? "" : String(v)])
|
|
6896
|
+
);
|
|
6897
|
+
for (const [source, reason] of Object.entries(res.unavailable ?? {})) {
|
|
6898
|
+
console.log(`
|
|
6899
|
+
unavailable: ${source} \u2014 ${reason}`);
|
|
6900
|
+
}
|
|
6901
|
+
} catch (e) {
|
|
6902
|
+
fail(e);
|
|
6903
|
+
}
|
|
6904
|
+
});
|
|
6905
|
+
var leadNextActionsCmd = leadsCmd.command("next-actions").description("Read the suggested next action for each lead, hold or close a lead, and dry-run the playbook on one lead");
|
|
6906
|
+
leadNextActionsCmd.command("list").description(
|
|
6907
|
+
"List each lead's current suggestion, highest priority first. --status pending_approval lists the cards waiting for a person."
|
|
6908
|
+
).option("--dataset <slug>", "narrow to one lead dataset").option(
|
|
6909
|
+
"--status <status>",
|
|
6910
|
+
"queued, pending_approval, executing, executed, approved (a text or call somebody said yes to, not yet carried out), rejected, expired, superseded, failed or recorded"
|
|
6911
|
+
).option("--priority <priority>", "high, medium or low").option("-l, --limit <n>", "maximum suggestions to return (default 50, maximum 200)", (v) => parseInt(v, 10)).option("--offset <n>", "skip that many suggestions, for paging", (v) => parseInt(v, 10)).option("--json", "print the raw JSON result instead of a table").action(
|
|
6912
|
+
async (opts) => {
|
|
6913
|
+
try {
|
|
6914
|
+
const res = await new ErdoClient().listLeadNextActions({
|
|
6915
|
+
dataset: opts.dataset,
|
|
6916
|
+
status: opts.status,
|
|
6917
|
+
priority: opts.priority,
|
|
6918
|
+
limit: opts.limit,
|
|
6919
|
+
offset: opts.offset
|
|
6920
|
+
});
|
|
6921
|
+
if (opts.json) {
|
|
6922
|
+
print(res);
|
|
6923
|
+
return;
|
|
6924
|
+
}
|
|
6925
|
+
const items = res.next_actions ?? [];
|
|
6926
|
+
if (items.length === 0) {
|
|
6927
|
+
console.log("No lead next actions match.");
|
|
6928
|
+
return;
|
|
6929
|
+
}
|
|
6930
|
+
printAlignedTable(
|
|
6931
|
+
["priority", "status", "action", "mode", "lead", "evaluated", "rationale"],
|
|
6932
|
+
items.map((a) => [
|
|
6933
|
+
a.priority,
|
|
6934
|
+
a.status,
|
|
6935
|
+
a.action_kind,
|
|
6936
|
+
a.mode,
|
|
6937
|
+
a.lead_reference,
|
|
6938
|
+
a.evaluated_at,
|
|
6939
|
+
clipLeadText(a.rationale, 80)
|
|
6940
|
+
])
|
|
6941
|
+
);
|
|
6942
|
+
if (items.length === res.limit) {
|
|
6943
|
+
console.log(`
|
|
6944
|
+
More may follow: --offset ${res.offset + res.limit}`);
|
|
6945
|
+
}
|
|
6946
|
+
} catch (e) {
|
|
6947
|
+
fail(e);
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
);
|
|
6951
|
+
leadNextActionsCmd.command("history <dataset> <lead>").description(
|
|
6952
|
+
"Show every decision about one lead, newest first: what was suggested, why, and what became of it. <lead> is the canonical_lead_id UUID or the 22-character lead reference."
|
|
6953
|
+
).option("--json", "print the raw JSON result instead of a table").action(async (dataset, lead, opts) => {
|
|
6954
|
+
try {
|
|
6955
|
+
const res = await new ErdoClient().getLeadNextActions(dataset, lead);
|
|
6956
|
+
if (opts.json) {
|
|
6957
|
+
print(res);
|
|
6958
|
+
return;
|
|
6959
|
+
}
|
|
6960
|
+
const items = res.next_actions ?? [];
|
|
6961
|
+
if (items.length === 0) {
|
|
6962
|
+
console.log("No decisions have been recorded for this lead.");
|
|
6963
|
+
return;
|
|
6964
|
+
}
|
|
6965
|
+
printAlignedTable(
|
|
6966
|
+
["evaluated", "source", "action", "mode", "status", "emailed", "rationale"],
|
|
6967
|
+
items.map((a) => [
|
|
6968
|
+
a.evaluated_at,
|
|
6969
|
+
a.source,
|
|
6970
|
+
a.action_kind,
|
|
6971
|
+
a.mode,
|
|
6972
|
+
a.status,
|
|
6973
|
+
// Whether the lead heard anything while this decision waited for a
|
|
6974
|
+
// person. On a call or a text card it is the difference between a
|
|
6975
|
+
// lead sitting in silence and one holding a booking link.
|
|
6976
|
+
a.accompanying_email?.status ?? "-",
|
|
6977
|
+
clipLeadText(a.rationale, 80)
|
|
6978
|
+
])
|
|
6979
|
+
);
|
|
6980
|
+
} catch (e) {
|
|
6981
|
+
fail(e);
|
|
6982
|
+
}
|
|
6983
|
+
});
|
|
6984
|
+
leadNextActionsCmd.command("hold <dataset> <lead>").description(
|
|
6985
|
+
"Tell Erdo the sales desk is handling this lead. The lead's open suggestion is replaced and any card it filed is withdrawn, so Erdo does not email someone the desk is already talking to. The hold ends at --until (default seven days, at most 180) or when the lead writes back."
|
|
6986
|
+
).option("--until <time>", "when the hold ends, as an RFC 3339 time, e.g. 2026-10-01T09:00:00-04:00").option("--note <text>", "why, in the desk's words").option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
|
|
6987
|
+
try {
|
|
6988
|
+
let until;
|
|
6989
|
+
if (opts.until) {
|
|
6990
|
+
const ms = Date.parse(opts.until);
|
|
6991
|
+
if (Number.isNaN(ms)) throw new Error(`--until is not a time: ${opts.until}`);
|
|
6992
|
+
until = new Date(ms).toISOString();
|
|
6993
|
+
}
|
|
6994
|
+
const res = await new ErdoClient().recordLeadNextAction(dataset, lead, { kind: "hold", until, note: opts.note });
|
|
6995
|
+
if (opts.json) {
|
|
6996
|
+
print(res);
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6999
|
+
printRecordedLeadDecision(res);
|
|
7000
|
+
} catch (e) {
|
|
7001
|
+
fail(e);
|
|
7002
|
+
}
|
|
7003
|
+
});
|
|
7004
|
+
leadNextActionsCmd.command("close <dataset> <lead>").description(
|
|
7005
|
+
"Stop working this lead. The lead's open suggestion is replaced and any card it filed is withdrawn; Erdo suggests nothing more until the lead gets in touch again."
|
|
7006
|
+
).option("--note <text>", "why, in the desk's words").option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
|
|
7007
|
+
try {
|
|
7008
|
+
const res = await new ErdoClient().recordLeadNextAction(dataset, lead, { kind: "close", note: opts.note });
|
|
7009
|
+
if (opts.json) {
|
|
7010
|
+
print(res);
|
|
7011
|
+
return;
|
|
7012
|
+
}
|
|
7013
|
+
printRecordedLeadDecision(res);
|
|
7014
|
+
} catch (e) {
|
|
7015
|
+
fail(e);
|
|
7016
|
+
}
|
|
7017
|
+
});
|
|
7018
|
+
leadNextActionsCmd.command("evaluate <dataset> <lead>").description(
|
|
7019
|
+
"Dry-run the playbook on one lead: show the decision Erdo would make now, with the email or handoff it would write, the rule it cites and why. Nothing is stored and nothing is sent. --file tries a draft playbook instead of the saved one."
|
|
7020
|
+
).option("-f, --file <path>", "a draft playbook text to evaluate with instead of the saved one").option("--agent-id <id>", "try the draft as that concierge, from `erdo voice agents`; only read alongside --file").option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
|
|
7021
|
+
try {
|
|
7022
|
+
const body = opts.file ? readFileSync4(opts.file, "utf8") : void 0;
|
|
7023
|
+
const res = await new ErdoClient().evaluateLeadNextAction(dataset, lead, body, opts.agentId);
|
|
7024
|
+
if (opts.json) {
|
|
7025
|
+
print(res);
|
|
7026
|
+
return;
|
|
7027
|
+
}
|
|
7028
|
+
const ev = res.evaluation;
|
|
7029
|
+
if (res.agent) console.log(`as ${res.agent.name} (${res.agent.slug})`);
|
|
7030
|
+
console.log(`stage ${ev.stage}`);
|
|
7031
|
+
console.log(`priority ${ev.priority}${ev.priority_reason ? ` \u2014 ${ev.priority_reason}` : ""}`);
|
|
7032
|
+
const proposed = ev.proposed_kind && ev.proposed_kind !== ev.action_kind ? ` (proposed ${ev.proposed_kind})` : "";
|
|
7033
|
+
console.log(`action ${ev.action_kind}${proposed}`);
|
|
7034
|
+
console.log(`mode ${ev.mode}`);
|
|
7035
|
+
if (ev.due_at) console.log(`due ${ev.due_at}`);
|
|
7036
|
+
console.log(`rule ${ev.rule ? `"${ev.rule}"` : "(none)"}${ev.rule_verified ? "" : " \u2014 not found in the playbook"}`);
|
|
7037
|
+
console.log(`
|
|
7038
|
+
${ev.rationale}`);
|
|
7039
|
+
const input = ev.action_input;
|
|
7040
|
+
if (input.email) {
|
|
7041
|
+
console.log(`
|
|
7042
|
+
email to ${input.to ?? "(no address)"}`);
|
|
7043
|
+
console.log(`subject: ${input.email.subject}
|
|
7044
|
+
`);
|
|
7045
|
+
console.log(input.email.body_markdown);
|
|
7046
|
+
}
|
|
7047
|
+
if (input.handoff) {
|
|
7048
|
+
const channel = input.handoff.suggested_channel ? ` (by ${input.handoff.suggested_channel})` : "";
|
|
7049
|
+
console.log(`
|
|
7050
|
+
handoff${channel}: ${input.handoff.summary}`);
|
|
7051
|
+
}
|
|
7052
|
+
if (input.sms) {
|
|
7053
|
+
console.log(`
|
|
7054
|
+
text to send:
|
|
7055
|
+
${input.sms.body}`);
|
|
7056
|
+
}
|
|
7057
|
+
if (input.agent_instructions) {
|
|
7058
|
+
console.log(`
|
|
7059
|
+
for the concierge \u2014 ${input.agent_instructions.objective}`);
|
|
7060
|
+
console.log(input.agent_instructions.instructions);
|
|
7061
|
+
}
|
|
7062
|
+
if (input.downgrade_cause) {
|
|
7063
|
+
console.log(`
|
|
7064
|
+
why a person decides: ${input.downgrade_cause}`);
|
|
7065
|
+
}
|
|
7066
|
+
} catch (e) {
|
|
7067
|
+
fail(e);
|
|
7068
|
+
}
|
|
7069
|
+
});
|
|
7070
|
+
function clipLeadText(text, max) {
|
|
7071
|
+
const flat = (text ?? "").replace(/\s+/g, " ").trim();
|
|
7072
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
7073
|
+
}
|
|
7074
|
+
function printLeadPlaybook(pb, opts = {}) {
|
|
7075
|
+
if (!pb.exists) {
|
|
7076
|
+
console.log("No playbook is saved. This is the default template; save one with `erdo leads playbook set`.");
|
|
7077
|
+
} else {
|
|
7078
|
+
console.log(`revision ${pb.revision}${pb.updated_at ? ` (saved ${pb.updated_at})` : ""}`);
|
|
7079
|
+
}
|
|
7080
|
+
console.log(`enabled ${pb.enabled ? "yes" : "no"}`);
|
|
7081
|
+
console.log(`daily cap ${pb.daily_contact_cap}`);
|
|
7082
|
+
if (pb.agent) {
|
|
7083
|
+
console.log(`concierge ${pb.agent.name} (${pb.agent.slug})${pb.agent.phone_number ? ` ${pb.agent.phone_number}` : ""}`);
|
|
7084
|
+
} else if (pb.agent_id) {
|
|
7085
|
+
console.log(`concierge ${pb.agent_id} \u2014 archived; pick another with \`erdo leads playbook set --agent-id\``);
|
|
7086
|
+
} else {
|
|
7087
|
+
console.log("concierge none \u2014 texts and calls cannot be proposed");
|
|
7088
|
+
}
|
|
7089
|
+
if (pb.exists) {
|
|
7090
|
+
console.log("");
|
|
7091
|
+
printLeadReadBack(pb.read_back);
|
|
7092
|
+
}
|
|
7093
|
+
if (opts.withBody !== false) {
|
|
7094
|
+
console.log("\n--- playbook ---");
|
|
7095
|
+
console.log(pb.body);
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
function printLeadReadBack(rb) {
|
|
7099
|
+
if (rb.actions?.length) {
|
|
7100
|
+
printAlignedTable(
|
|
7101
|
+
["action", "mode", "when"],
|
|
7102
|
+
rb.actions.map((a) => [a.kind, a.mode, clipLeadText(a.when, 80)])
|
|
7103
|
+
);
|
|
7104
|
+
}
|
|
7105
|
+
if (rb.working_hours) {
|
|
7106
|
+
const wh = rb.working_hours;
|
|
7107
|
+
console.log(`
|
|
7108
|
+
working hours ${wh.days.join(", ")} ${wh.start}\u2013${wh.end} ${wh.timezone}`);
|
|
7109
|
+
}
|
|
7110
|
+
if (rb.stages?.length) {
|
|
7111
|
+
console.log("\nstages");
|
|
7112
|
+
printAlignedTable(
|
|
7113
|
+
["stage", "actions", "summary"],
|
|
7114
|
+
rb.stages.map((s) => [s.name, s.actions.join(", "), clipLeadText(s.summary, 80)])
|
|
7115
|
+
);
|
|
7116
|
+
}
|
|
7117
|
+
if (rb.priority) {
|
|
7118
|
+
console.log("\npriority Erdo read from the text (not enforced)");
|
|
7119
|
+
printAlignedTable(
|
|
7120
|
+
["level", "conditions"],
|
|
7121
|
+
[
|
|
7122
|
+
["high", rb.priority.high.join("; ") || "\u2014"],
|
|
7123
|
+
["medium", rb.priority.medium.join("; ") || "\u2014"],
|
|
7124
|
+
["low", rb.priority.low.join("; ") || "\u2014"]
|
|
7125
|
+
]
|
|
7126
|
+
);
|
|
7127
|
+
if (rb.priority.forbidden_factors?.length) {
|
|
7128
|
+
console.log(`
|
|
7129
|
+
must not affect priority ${rb.priority.forbidden_factors.join(", ")}`);
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
if (rb.limits?.length) {
|
|
7133
|
+
console.log("\nlimits Erdo applied");
|
|
7134
|
+
for (const l of rb.limits) console.log(` ${l.kind} \u2192 ${l.mode}: ${l.reason}`);
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
function printLeadPlaybookRevision(rev) {
|
|
7138
|
+
console.log(`revision ${rev.revision} (saved ${rev.saved_at}${rev.saved_by ? ` by ${rev.saved_by}` : ""})`);
|
|
7139
|
+
console.log(`enabled ${rev.enabled ? "yes" : "no"}`);
|
|
7140
|
+
console.log(`daily cap ${rev.daily_contact_cap}`);
|
|
7141
|
+
console.log(`concierge ${rev.agent_id ?? "none"}`);
|
|
7142
|
+
console.log("");
|
|
7143
|
+
printLeadReadBack(rev.read_back);
|
|
7144
|
+
console.log("\n--- playbook as it was saved ---");
|
|
7145
|
+
console.log(rev.body);
|
|
7146
|
+
}
|
|
7147
|
+
function printRecordedLeadDecision(a) {
|
|
7148
|
+
const until = a.revisit_at ? ` until ${a.revisit_at}` : "";
|
|
7149
|
+
console.log(`Recorded ${a.action_kind} on lead ${a.lead_reference}${a.action_kind === "hold" ? until : ""}.`);
|
|
7150
|
+
if (a.rationale) console.log(a.rationale);
|
|
7151
|
+
}
|
|
6308
7152
|
function printLead(lead) {
|
|
6309
7153
|
console.log(`lead ${lead.canonical_lead_id}`);
|
|
6310
7154
|
console.log(`reference ${lead.reference}`);
|
|
@@ -6520,7 +7364,7 @@ List tables with: erdo integrations tables ${app} <schema>`);
|
|
|
6520
7364
|
return;
|
|
6521
7365
|
}
|
|
6522
7366
|
for (const t of res.tables ?? []) {
|
|
6523
|
-
const rows = t.estimated_row_count
|
|
7367
|
+
const rows = t.estimated_row_count == null ? "" : ` ~${t.estimated_row_count} rows`;
|
|
6524
7368
|
console.log(`${t.schema_name}.${t.table_name} ${t.columns.length} columns${rows}`);
|
|
6525
7369
|
}
|
|
6526
7370
|
} catch (e) {
|