@erdoai/cli 0.90.0 → 0.98.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 +424 -61
- 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",
|
|
@@ -495,6 +510,8 @@ var ErdoClient = class {
|
|
|
495
510
|
const q = new URLSearchParams();
|
|
496
511
|
if (params?.agent) q.set("agent", params.agent);
|
|
497
512
|
if (params?.direction) q.set("direction", params.direction);
|
|
513
|
+
if (params?.since) q.set("since", params.since);
|
|
514
|
+
if (params?.until) q.set("until", params.until);
|
|
498
515
|
if (params?.canonical_lead_id) q.set("canonical_lead_id", params.canonical_lead_id);
|
|
499
516
|
if (params?.limit !== void 0) q.set("limit", String(params.limit));
|
|
500
517
|
if (params?.offset !== void 0) q.set("offset", String(params.offset));
|
|
@@ -602,6 +619,16 @@ var ErdoClient = class {
|
|
|
602
619
|
{ enabled }
|
|
603
620
|
);
|
|
604
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
|
+
}
|
|
605
632
|
// Run a read-only HogQL query against the org's page-analytics events. Rows are
|
|
606
633
|
// positional per columns; enabled:false means page analytics is off for the org
|
|
607
634
|
// (not zero traffic). A rejected query surfaces PostHog's message as the error.
|
|
@@ -882,6 +909,9 @@ var ErdoClient = class {
|
|
|
882
909
|
for (const s of params?.statuses ?? []) q.append("statuses", s);
|
|
883
910
|
if (params?.engine_actions_only) q.set("engine_actions_only", "true");
|
|
884
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);
|
|
885
915
|
if (params?.limit) q.set("limit", String(params.limit));
|
|
886
916
|
if (params?.offset) q.set("offset", String(params.offset));
|
|
887
917
|
const qs = q.toString();
|
|
@@ -1980,6 +2010,29 @@ function timedOutMessage(threadID) {
|
|
|
1980
2010
|
function print(value) {
|
|
1981
2011
|
console.log(JSON.stringify(value, null, 2));
|
|
1982
2012
|
}
|
|
2013
|
+
function formatNumber(n) {
|
|
2014
|
+
if (!Number.isFinite(n)) return String(n);
|
|
2015
|
+
if (Number.isInteger(n)) return n.toLocaleString("en-US");
|
|
2016
|
+
if (Math.abs(n) < 1e-6) {
|
|
2017
|
+
return n.toFixed(10).replace(/0+$/, "").replace(/\.$/, "");
|
|
2018
|
+
}
|
|
2019
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
2020
|
+
}
|
|
2021
|
+
function formatDate(iso) {
|
|
2022
|
+
if (!iso) return void 0;
|
|
2023
|
+
const d = new Date(iso);
|
|
2024
|
+
if (isNaN(d.getTime())) return iso;
|
|
2025
|
+
return `${d.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
2026
|
+
}
|
|
2027
|
+
function compactJSON(value) {
|
|
2028
|
+
if (value === void 0 || value === null) return void 0;
|
|
2029
|
+
try {
|
|
2030
|
+
const s = JSON.stringify(value);
|
|
2031
|
+
return s === "null" ? void 0 : s;
|
|
2032
|
+
} catch {
|
|
2033
|
+
return String(value);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1983
2036
|
async function readAllStdin() {
|
|
1984
2037
|
if (process.stdin.isTTY) {
|
|
1985
2038
|
throw new Error("nothing is piped to standard input");
|
|
@@ -2028,7 +2081,11 @@ function printPageReview(review) {
|
|
|
2028
2081
|
}
|
|
2029
2082
|
}
|
|
2030
2083
|
function printAlignedTable(columns, rows) {
|
|
2031
|
-
const cell = (v) =>
|
|
2084
|
+
const cell = (v) => {
|
|
2085
|
+
if (v === null || v === void 0) return "";
|
|
2086
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
2087
|
+
return String(v);
|
|
2088
|
+
};
|
|
2032
2089
|
const widths = columns.map((c, i) => Math.max(c.length, ...rows.map((r) => cell(r[i]).length), 0));
|
|
2033
2090
|
const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
2034
2091
|
console.log(line(columns));
|
|
@@ -2043,7 +2100,10 @@ var collect = (v, acc) => {
|
|
|
2043
2100
|
function summariseResults(results, cases = []) {
|
|
2044
2101
|
const nameByID = new Map(cases.map((c) => [c.id, c.name]));
|
|
2045
2102
|
for (const r of results) {
|
|
2046
|
-
|
|
2103
|
+
let status;
|
|
2104
|
+
if (r.agent_error) status = "ERROR";
|
|
2105
|
+
else if (r.passed) status = "PASS";
|
|
2106
|
+
else status = "FAIL";
|
|
2047
2107
|
const name = nameByID.get(r.case_id) ?? r.case_id;
|
|
2048
2108
|
console.log(` [${status}] ${r.score.toFixed(2)} ${name}`);
|
|
2049
2109
|
if (r.agent_error) console.log(` agent error: ${r.agent_error}`);
|
|
@@ -2271,6 +2331,7 @@ function printBusinessProfile(profile) {
|
|
|
2271
2331
|
["industry", profile.industry],
|
|
2272
2332
|
["privacy policy", profile.privacy_policy_url],
|
|
2273
2333
|
["terms", profile.terms_and_conditions_url],
|
|
2334
|
+
["example landing page", profile.example_landing_page_url],
|
|
2274
2335
|
["representative", `${profile.representative_first_name} ${profile.representative_last_name}`.trim()],
|
|
2275
2336
|
["title", profile.representative_title],
|
|
2276
2337
|
["job position", profile.representative_job_position],
|
|
@@ -2322,7 +2383,10 @@ businessProfileCmd.command("set").description("Save the organization's business
|
|
|
2322
2383
|
).option(
|
|
2323
2384
|
"--tax-id-stdin",
|
|
2324
2385
|
"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`"
|
|
2325
|
-
).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(
|
|
2386
|
+
).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(
|
|
2387
|
+
"--example-landing-page-url <url>",
|
|
2388
|
+
"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"
|
|
2389
|
+
).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(
|
|
2326
2390
|
"--job-position <position>",
|
|
2327
2391
|
"their role from the fixed list (ceo, cfo, director, general_counsel, gm, vp, other)"
|
|
2328
2392
|
).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(
|
|
@@ -2360,6 +2424,7 @@ businessProfileCmd.command("set").description("Save the organization's business
|
|
|
2360
2424
|
industry: opts.industry ?? stored.industry,
|
|
2361
2425
|
privacy_policy_url: opts.privacyPolicyUrl ?? stored.privacy_policy_url,
|
|
2362
2426
|
terms_and_conditions_url: opts.termsUrl ?? stored.terms_and_conditions_url,
|
|
2427
|
+
example_landing_page_url: opts.exampleLandingPageUrl ?? stored.example_landing_page_url ?? "",
|
|
2363
2428
|
representative_first_name: opts.firstName ?? stored.representative_first_name,
|
|
2364
2429
|
representative_last_name: opts.lastName ?? stored.representative_last_name,
|
|
2365
2430
|
representative_title: opts.title ?? stored.representative_title,
|
|
@@ -2520,9 +2585,11 @@ managed.command("ads-container <orgSlug>").description(
|
|
|
2520
2585
|
timeZone: opts.timeZone,
|
|
2521
2586
|
replaceCurrentAccount: opts.replaceCurrentAccount
|
|
2522
2587
|
});
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
)
|
|
2588
|
+
let outcome;
|
|
2589
|
+
if (c.already_provisioned) outcome = `Google Ads container already existed for ${c.org_slug}`;
|
|
2590
|
+
else if (c.adopted) outcome = `Adopted Google Ads account ${c.customer_id} as the container for ${c.org_slug}`;
|
|
2591
|
+
else outcome = `Created Google Ads container for ${c.org_slug}`;
|
|
2592
|
+
console.log(outcome);
|
|
2526
2593
|
printAdsContainer(c);
|
|
2527
2594
|
const leftBehind = c.previous_campaign_count ?? 0;
|
|
2528
2595
|
if (c.previous_customer_id && leftBehind > 0) {
|
|
@@ -2581,6 +2648,90 @@ This key operates every org you manage; target one with X-Organization-ID (or \`
|
|
|
2581
2648
|
fail(e);
|
|
2582
2649
|
}
|
|
2583
2650
|
});
|
|
2651
|
+
managed.command("performance").description(
|
|
2652
|
+
"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."
|
|
2653
|
+
).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(
|
|
2654
|
+
async (opts) => {
|
|
2655
|
+
try {
|
|
2656
|
+
const res = await new ErdoClient().managerPerformance({
|
|
2657
|
+
organization_slug: opts.organization,
|
|
2658
|
+
from: opts.from,
|
|
2659
|
+
to: opts.to
|
|
2660
|
+
});
|
|
2661
|
+
if (opts.json) {
|
|
2662
|
+
print(res);
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
if (res.organizations.length === 0) {
|
|
2666
|
+
console.log(res.message || "No organizations to read.");
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
const cell = (v, digits = 0) => v === null ? "-" : v.toFixed(digits);
|
|
2670
|
+
const total = (days, pick) => {
|
|
2671
|
+
let sum = 0;
|
|
2672
|
+
for (const day of days) {
|
|
2673
|
+
const v = pick(day);
|
|
2674
|
+
if (v === null) return null;
|
|
2675
|
+
sum += v;
|
|
2676
|
+
}
|
|
2677
|
+
return sum;
|
|
2678
|
+
};
|
|
2679
|
+
if (opts.daily) {
|
|
2680
|
+
const rows = [];
|
|
2681
|
+
for (const org2 of res.organizations) {
|
|
2682
|
+
for (const day of org2.days) {
|
|
2683
|
+
rows.push([
|
|
2684
|
+
org2.organization_slug,
|
|
2685
|
+
day.date,
|
|
2686
|
+
cell(day.spend, 2),
|
|
2687
|
+
cell(day.clicks),
|
|
2688
|
+
cell(day.visits),
|
|
2689
|
+
cell(day.form_starts),
|
|
2690
|
+
cell(day.leads_on_page),
|
|
2691
|
+
cell(day.leads_captured)
|
|
2692
|
+
]);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
printAlignedTable(
|
|
2696
|
+
["organization", "date", "spend", "clicks", "visits", "form starts", "page leads", "leads"],
|
|
2697
|
+
rows
|
|
2698
|
+
);
|
|
2699
|
+
} else {
|
|
2700
|
+
printAlignedTable(
|
|
2701
|
+
["organization", "spend", "clicks", "visits", "form starts", "page leads", "leads", "cost/lead"],
|
|
2702
|
+
res.organizations.map((org2) => {
|
|
2703
|
+
const spend = total(org2.days, (d) => d.spend);
|
|
2704
|
+
const leads = total(org2.days, (d) => d.leads_captured);
|
|
2705
|
+
const costPerLead = spend === null || leads === null || leads === 0 ? null : spend / leads;
|
|
2706
|
+
return [
|
|
2707
|
+
org2.organization_slug,
|
|
2708
|
+
cell(spend, 2),
|
|
2709
|
+
cell(total(org2.days, (d) => d.clicks)),
|
|
2710
|
+
cell(total(org2.days, (d) => d.visits)),
|
|
2711
|
+
cell(total(org2.days, (d) => d.form_starts)),
|
|
2712
|
+
cell(total(org2.days, (d) => d.leads_on_page)),
|
|
2713
|
+
cell(leads, 0),
|
|
2714
|
+
cell(costPerLead, 2)
|
|
2715
|
+
];
|
|
2716
|
+
})
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
process.stderr.write(`
|
|
2720
|
+
${res.from} to ${res.to} (${res.timezone} days)
|
|
2721
|
+
`);
|
|
2722
|
+
for (const org2 of res.organizations) {
|
|
2723
|
+
for (const gap of org2.coverage) {
|
|
2724
|
+
process.stderr.write(
|
|
2725
|
+
`${org2.organization_slug}: ${gap.metrics.join(", ")} unavailable \u2014 ${gap.reason}
|
|
2726
|
+
`
|
|
2727
|
+
);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
fail(e);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
);
|
|
2584
2735
|
var tokenCmd = program.command("token").description("Manage your API tokens (account-level credentials)");
|
|
2585
2736
|
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(
|
|
2586
2737
|
orgOption("the token's default org (defaults to your active org; must be one you belong to)")
|
|
@@ -2869,7 +3020,7 @@ function splitVariantSpec(spec) {
|
|
|
2869
3020
|
const key = eq > 0 ? trimmed.slice(0, eq).trim() : "";
|
|
2870
3021
|
const startsToken = trimmed === "control" || trimmed === "is_control" || key !== "" && VARIANT_SPEC_KEYS.has(key);
|
|
2871
3022
|
if (!startsToken && parts.length > 0 && COMMA_SAFE_VARIANT_KEYS.has(lastKey)) {
|
|
2872
|
-
parts[parts.length - 1] +=
|
|
3023
|
+
parts[parts.length - 1] += `,${raw}`;
|
|
2873
3024
|
continue;
|
|
2874
3025
|
}
|
|
2875
3026
|
parts.push(raw);
|
|
@@ -3379,9 +3530,13 @@ ${"Case".padEnd(34)}${res.models.map(modelCol).join("")}`);
|
|
|
3379
3530
|
console.log(`
|
|
3380
3531
|
${"Summary (avg of all cases)".padEnd(34)}${res.models.map(modelCol).join("")}`);
|
|
3381
3532
|
for (const stat of ["score", "pass", "cost", "secs"]) {
|
|
3382
|
-
let line =
|
|
3533
|
+
let line = ` ${stat}`.padEnd(34);
|
|
3383
3534
|
for (const s of res.summary) {
|
|
3384
|
-
|
|
3535
|
+
let v;
|
|
3536
|
+
if (stat === "score") v = s.avg_score.toFixed(2);
|
|
3537
|
+
else if (stat === "pass") v = `${Math.round(s.pass_rate * 100)}%`;
|
|
3538
|
+
else if (stat === "cost") v = `$${(s.avg_cost_millicents / 1e5).toFixed(3)}/case`;
|
|
3539
|
+
else v = `${(s.avg_duration_ms / 1e3).toFixed(0)}s`;
|
|
3385
3540
|
line += v.padEnd(18);
|
|
3386
3541
|
}
|
|
3387
3542
|
console.log(line);
|
|
@@ -3729,9 +3884,11 @@ function printRunDetail(detail, wantResources, wantSteps) {
|
|
|
3729
3884
|
group.set(key, entry);
|
|
3730
3885
|
byKind.set(kind, group);
|
|
3731
3886
|
}
|
|
3732
|
-
const kinds = [...byKind.keys()].sort(
|
|
3733
|
-
(a
|
|
3734
|
-
|
|
3887
|
+
const kinds = [...byKind.keys()].sort((a, b) => {
|
|
3888
|
+
if (a === "skill") return -1;
|
|
3889
|
+
if (b === "skill") return 1;
|
|
3890
|
+
return a.localeCompare(b);
|
|
3891
|
+
});
|
|
3735
3892
|
for (const kind of kinds) {
|
|
3736
3893
|
console.log(` ${kind}`);
|
|
3737
3894
|
for (const entry of byKind.get(kind).values()) {
|
|
@@ -3809,6 +3966,46 @@ function decisionLine(d) {
|
|
|
3809
3966
|
}
|
|
3810
3967
|
return parts.join(" ");
|
|
3811
3968
|
}
|
|
3969
|
+
function renderApprovalCard(approval) {
|
|
3970
|
+
console.log("");
|
|
3971
|
+
console.log("Approval:");
|
|
3972
|
+
const headline = approval.action_display || approval.action_headline;
|
|
3973
|
+
if (headline) console.log(` ${headline}`);
|
|
3974
|
+
if (approval.action_context) console.log(` ${approval.action_context}`);
|
|
3975
|
+
for (const item of approval.items ?? []) {
|
|
3976
|
+
const params = item.params ? Object.entries(item.params).map(([k, v]) => `${k}=${v}`).join(", ") : "";
|
|
3977
|
+
console.log(` - ${item.what}${params ? ` (${params})` : ""}`);
|
|
3978
|
+
if (item.why) console.log(` why: ${item.why}`);
|
|
3979
|
+
}
|
|
3980
|
+
if (approval.omitted_items) console.log(` and ${approval.omitted_items} more`);
|
|
3981
|
+
if (approval.occurrence_count && approval.occurrence_count > 1) {
|
|
3982
|
+
console.log(` proposed ${approval.occurrence_count}x`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
function renderReportEffect(e) {
|
|
3986
|
+
if (e.measurability !== "measurable") {
|
|
3987
|
+
const reason = e.unmeasurable_reason ? `: ${e.unmeasurable_reason}` : "";
|
|
3988
|
+
console.log(` ${e.metric || e.measurability} \u2014 ${e.measurability}${reason}`);
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
const predicateValue = e.target_value ?? e.min_delta;
|
|
3992
|
+
const predicate = [
|
|
3993
|
+
e.metric,
|
|
3994
|
+
e.success_operator,
|
|
3995
|
+
predicateValue !== void 0 && predicateValue !== null ? formatNumber(predicateValue) : void 0
|
|
3996
|
+
].filter((p) => p !== void 0 && p !== null && p !== "").join(" ");
|
|
3997
|
+
const windows = [];
|
|
3998
|
+
if (e.baseline_window_days) windows.push(`baseline ${e.baseline_window_days}d`);
|
|
3999
|
+
if (e.outcome_window_days) windows.push(`outcome ${e.outcome_window_days}d`);
|
|
4000
|
+
const windowStr = windows.length ? ` (${windows.join(", ")})` : "";
|
|
4001
|
+
console.log(` ${predicate || e.metric || "effect"}${windowStr} \u2014 ${e.status}`);
|
|
4002
|
+
if (e.baseline_value !== void 0 && e.baseline_value !== null && e.outcome_value !== void 0 && e.outcome_value !== null) {
|
|
4003
|
+
const label = e.outcome_label ? ` (${e.outcome_label})` : "";
|
|
4004
|
+
console.log(` measured: ${formatNumber(e.baseline_value)} \u2192 ${formatNumber(e.outcome_value)}${label}`);
|
|
4005
|
+
} else if (e.outcome_label) {
|
|
4006
|
+
console.log(` outcome: ${e.outcome_label}`);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
3812
4009
|
decisionsCmd.command("list").description("List decisions, newest first").option("--workstream <slug>", "only decisions filed under this workstream/Strategy").option(
|
|
3813
4010
|
"--source <source>",
|
|
3814
4011
|
"producer family: approval | escalation | engine_gate | allocator | experiment | workstream_commitment"
|
|
@@ -3868,6 +4065,7 @@ function renderDecision(detail) {
|
|
|
3868
4065
|
console.log("");
|
|
3869
4066
|
console.log(`Why: ${d.why}`);
|
|
3870
4067
|
}
|
|
4068
|
+
if (d.alternatives_considered) console.log(`Alternatives considered: ${d.alternatives_considered}`);
|
|
3871
4069
|
if (d.decider_rationale) console.log(`Decider said: ${d.decider_rationale}`);
|
|
3872
4070
|
if (detail.actions.length) {
|
|
3873
4071
|
console.log("");
|
|
@@ -3877,6 +4075,12 @@ function renderDecision(detail) {
|
|
|
3877
4075
|
if (a.subject_label) console.log(` subject: ${a.subject_label}`);
|
|
3878
4076
|
if (a.effective_at) console.log(` effective: ${a.effective_at}`);
|
|
3879
4077
|
if (a.result_summary) console.log(` result: ${a.result_summary}`);
|
|
4078
|
+
if (a.annotations && Object.keys(a.annotations).length) {
|
|
4079
|
+
console.log(" before:");
|
|
4080
|
+
for (const [k, v] of Object.entries(a.annotations)) {
|
|
4081
|
+
console.log(` ${k}: ${compactJSON(v) ?? "null"}`);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
3880
4084
|
});
|
|
3881
4085
|
}
|
|
3882
4086
|
if (detail.effects.length) {
|
|
@@ -3894,13 +4098,28 @@ function renderDecision(detail) {
|
|
|
3894
4098
|
if (e.unmeasurable_reason) console.log(` reason: ${e.unmeasurable_reason}`);
|
|
3895
4099
|
});
|
|
3896
4100
|
}
|
|
3897
|
-
|
|
4101
|
+
const lineage = detail.lineage;
|
|
4102
|
+
if (lineage.supersedes_slug || lineage.superseded_by_slugs?.length || lineage.workstream_slug || lineage.attention_item_slug) {
|
|
3898
4103
|
console.log("");
|
|
3899
4104
|
console.log("Lineage:");
|
|
3900
|
-
if (
|
|
3901
|
-
for (const slug of
|
|
4105
|
+
if (lineage.supersedes_slug) console.log(` replaced ${lineage.supersedes_slug}`);
|
|
4106
|
+
for (const slug of lineage.superseded_by_slugs ?? []) {
|
|
3902
4107
|
console.log(` replaced by ${slug}`);
|
|
3903
4108
|
}
|
|
4109
|
+
if (lineage.workstream_slug) {
|
|
4110
|
+
console.log(` workstream: ${lineage.workstream_title || lineage.workstream_slug} (${lineage.workstream_slug})`);
|
|
4111
|
+
}
|
|
4112
|
+
if (lineage.attention_item_slug) {
|
|
4113
|
+
console.log(
|
|
4114
|
+
` raised by: ${lineage.attention_item_title || lineage.attention_item_slug} (${lineage.attention_item_slug})`
|
|
4115
|
+
);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
if (lineage.approval) renderApprovalCard(lineage.approval);
|
|
4119
|
+
else if (lineage.approval_unreadable) {
|
|
4120
|
+
console.log("");
|
|
4121
|
+
console.log("Approval:");
|
|
4122
|
+
console.log(" The approval card could not be loaded.");
|
|
3904
4123
|
}
|
|
3905
4124
|
}
|
|
3906
4125
|
decisionsCmd.command("show <slug>").description(
|
|
@@ -4174,9 +4393,7 @@ approvalsCmd.command("settings [mode]").description(
|
|
|
4174
4393
|
try {
|
|
4175
4394
|
const client = new ErdoClient();
|
|
4176
4395
|
let gates;
|
|
4177
|
-
if (opts.gates
|
|
4178
|
-
gates = opts.gates.split(",").map((g) => g.trim()).filter(Boolean);
|
|
4179
|
-
} else {
|
|
4396
|
+
if (opts.gates === void 0) {
|
|
4180
4397
|
switch (mode) {
|
|
4181
4398
|
case void 0:
|
|
4182
4399
|
break;
|
|
@@ -4193,6 +4410,8 @@ approvalsCmd.command("settings [mode]").description(
|
|
|
4193
4410
|
default:
|
|
4194
4411
|
throw new Error(`unknown mode ${mode} \u2014 use safe | all | reset, or --gates spend,destructive`);
|
|
4195
4412
|
}
|
|
4413
|
+
} else {
|
|
4414
|
+
gates = opts.gates.split(",").map((g) => g.trim()).filter(Boolean);
|
|
4196
4415
|
}
|
|
4197
4416
|
if (gates !== void 0) {
|
|
4198
4417
|
await client.setApprovalSettings(gates);
|
|
@@ -4210,19 +4429,100 @@ approvalsCmd.command("settings [mode]").description(
|
|
|
4210
4429
|
}
|
|
4211
4430
|
});
|
|
4212
4431
|
var attnCmd = program.command("attention").description("The attention feed \u2014 digests, choices, escalations awaiting a human");
|
|
4213
|
-
|
|
4432
|
+
function attentionItemLine(item) {
|
|
4433
|
+
return [item.slug, item.severity, item.kind, item.status, item.title].join(" ");
|
|
4434
|
+
}
|
|
4435
|
+
function renderAttentionItemReport(item, report) {
|
|
4436
|
+
console.log("");
|
|
4437
|
+
console.log("Safe default:");
|
|
4438
|
+
const expires = formatDate(item.expires_at);
|
|
4439
|
+
if (report.proposed_answer) {
|
|
4440
|
+
if (!expires) {
|
|
4441
|
+
console.log(` Erdo proposes: ${report.proposed_answer}`);
|
|
4442
|
+
console.log(" No deadline \u2014 nothing applies until somebody answers");
|
|
4443
|
+
} else if (report.safe_default_approves) {
|
|
4444
|
+
console.log(` If nobody answers: ${report.proposed_answer}`);
|
|
4445
|
+
console.log(` Applies ${expires}`);
|
|
4446
|
+
} else {
|
|
4447
|
+
console.log(` Erdo proposes: ${report.proposed_answer}`);
|
|
4448
|
+
console.log(` If nobody answers, Erdo will NOT do this \u2014 the item closes ${expires}`);
|
|
4449
|
+
}
|
|
4450
|
+
} else {
|
|
4451
|
+
console.log(expires ? ` Closes unanswered ${expires}` : " Does not expire");
|
|
4452
|
+
}
|
|
4453
|
+
if (report.has_pending_action) console.log(" Holds a pending engine action");
|
|
4454
|
+
const safeDefault = compactJSON(report.safe_default);
|
|
4455
|
+
if (safeDefault) console.log(` Default answer: ${safeDefault}`);
|
|
4456
|
+
if (report.workstream_slug || report.workstream_title) {
|
|
4457
|
+
console.log("");
|
|
4458
|
+
console.log("Workstream:");
|
|
4459
|
+
console.log(` ${report.workstream_title || report.workstream_slug} (${report.workstream_slug})`);
|
|
4460
|
+
}
|
|
4461
|
+
if (report.decision_slug) {
|
|
4462
|
+
console.log("");
|
|
4463
|
+
console.log("Decision:");
|
|
4464
|
+
if (report.decision) {
|
|
4465
|
+
const d = report.decision;
|
|
4466
|
+
console.log(` ${d.slug} \u2014 ${d.what}`);
|
|
4467
|
+
if (d.why) console.log(` why: ${d.why}`);
|
|
4468
|
+
if (d.alternatives_considered) console.log(` alternatives considered: ${d.alternatives_considered}`);
|
|
4469
|
+
console.log(` decided by: ${d.decider_kind}`);
|
|
4470
|
+
console.log(` status: ${d.status}`);
|
|
4471
|
+
if (d.outcome_label) console.log(` outcome: ${d.outcome_label}`);
|
|
4472
|
+
} else {
|
|
4473
|
+
console.log(` ${report.decision_slug} \u2014 exists, but in a project you cannot view`);
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
if (report.approval) renderApprovalCard(report.approval);
|
|
4477
|
+
else if (report.approval_unreadable) {
|
|
4478
|
+
console.log("");
|
|
4479
|
+
console.log("Approval:");
|
|
4480
|
+
console.log(" The approval card could not be loaded.");
|
|
4481
|
+
}
|
|
4482
|
+
if (report.effects?.length) {
|
|
4483
|
+
console.log("");
|
|
4484
|
+
console.log("Predicted vs measured:");
|
|
4485
|
+
for (const e of report.effects) renderReportEffect(e);
|
|
4486
|
+
}
|
|
4487
|
+
if (report.related_items?.length) {
|
|
4488
|
+
console.log("");
|
|
4489
|
+
console.log("Earlier items:");
|
|
4490
|
+
for (const r of report.related_items) {
|
|
4491
|
+
const reasons = [r.same_workstream ? "same workstream" : void 0, r.same_title ? "same title" : void 0].filter(Boolean).join(", ");
|
|
4492
|
+
const created = formatDate(r.created_at) ?? r.created_at;
|
|
4493
|
+
console.log(
|
|
4494
|
+
` [${r.severity}] ${r.status} \u2014 ${r.title} (${r.slug}) \u2014 ${created}${reasons ? ` \u2014 ${reasons}` : ""}`
|
|
4495
|
+
);
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
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(
|
|
4500
|
+
"--subject-kind <kind>",
|
|
4501
|
+
"only items about this kind of thing: google_ads_campaign | meta_ads_campaign | google_ads_ad_group | google_ads_ad | page | dataset | workstream"
|
|
4502
|
+
).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(
|
|
4214
4503
|
async (opts) => {
|
|
4215
4504
|
try {
|
|
4216
4505
|
const statuses = opts.status?.length ? opts.status : opts.open ? ["open"] : void 0;
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4506
|
+
const res = await new ErdoClient().listAttentionItems({
|
|
4507
|
+
statuses,
|
|
4508
|
+
engine_actions_only: opts.engineActions,
|
|
4509
|
+
item_id: opts.item,
|
|
4510
|
+
organization_slug: opts.org,
|
|
4511
|
+
subject_kind: opts.subjectKind,
|
|
4512
|
+
subject_id: opts.subjectId,
|
|
4513
|
+
limit: opts.limit,
|
|
4514
|
+
offset: opts.offset
|
|
4515
|
+
});
|
|
4516
|
+
if (opts.json) {
|
|
4517
|
+
print(res);
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
if (!res.items.length) {
|
|
4521
|
+
console.log("No attention items match. Widen the filters.");
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
for (const item of res.items) console.log(attentionItemLine(item));
|
|
4525
|
+
if (res.report) renderAttentionItemReport(res.items[0], res.report);
|
|
4226
4526
|
} catch (e) {
|
|
4227
4527
|
fail(e);
|
|
4228
4528
|
}
|
|
@@ -4233,7 +4533,7 @@ attnCmd.command("respond <id>").description(
|
|
|
4233
4533
|
).option("--answer <json>", `a JSON answer, e.g. '{"q1":["option-a"]}'`).option(
|
|
4234
4534
|
"--flag-broken <json>",
|
|
4235
4535
|
'flag choice variants as broken: JSON array [{"variant_key":"...","reason":"..."}]'
|
|
4236
|
-
).option("--ack", "mark read / acknowledge").option("--dismiss", "dismiss the item").action(
|
|
4536
|
+
).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(
|
|
4237
4537
|
async (id, opts) => {
|
|
4238
4538
|
try {
|
|
4239
4539
|
if (opts.ack || opts.dismiss) {
|
|
@@ -4245,7 +4545,8 @@ attnCmd.command("respond <id>").description(
|
|
|
4245
4545
|
}
|
|
4246
4546
|
print(
|
|
4247
4547
|
await new ErdoClient().respondAttentionItem(id, {
|
|
4248
|
-
action: opts.ack ? "acknowledge" : "dismiss"
|
|
4548
|
+
action: opts.ack ? "acknowledge" : "dismiss",
|
|
4549
|
+
organization_slug: opts.org
|
|
4249
4550
|
})
|
|
4250
4551
|
);
|
|
4251
4552
|
return;
|
|
@@ -4253,7 +4554,7 @@ attnCmd.command("respond <id>").description(
|
|
|
4253
4554
|
if (opts.answer === void 0 && opts.flagBroken === void 0) {
|
|
4254
4555
|
fail(new Error("provide one of --answer, --flag-broken, --ack, or --dismiss"));
|
|
4255
4556
|
}
|
|
4256
|
-
const input = { action: "answer" };
|
|
4557
|
+
const input = { action: "answer", organization_slug: opts.org };
|
|
4257
4558
|
if (opts.answer !== void 0) {
|
|
4258
4559
|
try {
|
|
4259
4560
|
input.answer = JSON.parse(opts.answer);
|
|
@@ -4309,7 +4610,19 @@ pageFeedbackCmd.command("record").description(
|
|
|
4309
4610
|
);
|
|
4310
4611
|
}
|
|
4311
4612
|
let items;
|
|
4312
|
-
if (opts.items
|
|
4613
|
+
if (opts.items === void 0) {
|
|
4614
|
+
if (!opts.experiment || !opts.variant || !opts.feedback) {
|
|
4615
|
+
throw new Error("provide --items, or all of --experiment, --variant, --feedback");
|
|
4616
|
+
}
|
|
4617
|
+
items = [
|
|
4618
|
+
{
|
|
4619
|
+
experiment_slug: opts.experiment,
|
|
4620
|
+
variant_key: opts.variant,
|
|
4621
|
+
feedback: opts.feedback,
|
|
4622
|
+
image_bucket_key: opts.imageKey
|
|
4623
|
+
}
|
|
4624
|
+
];
|
|
4625
|
+
} else {
|
|
4313
4626
|
let parsed;
|
|
4314
4627
|
try {
|
|
4315
4628
|
parsed = JSON.parse(readMaybeFile(opts.items));
|
|
@@ -4322,18 +4635,6 @@ pageFeedbackCmd.command("record").description(
|
|
|
4322
4635
|
throw new Error("--items must be a JSON array of {experiment_slug, variant_key, feedback} objects");
|
|
4323
4636
|
}
|
|
4324
4637
|
items = parsed;
|
|
4325
|
-
} else {
|
|
4326
|
-
if (!opts.experiment || !opts.variant || !opts.feedback) {
|
|
4327
|
-
throw new Error("provide --items, or all of --experiment, --variant, --feedback");
|
|
4328
|
-
}
|
|
4329
|
-
items = [
|
|
4330
|
-
{
|
|
4331
|
-
experiment_slug: opts.experiment,
|
|
4332
|
-
variant_key: opts.variant,
|
|
4333
|
-
feedback: opts.feedback,
|
|
4334
|
-
image_bucket_key: opts.imageKey
|
|
4335
|
-
}
|
|
4336
|
-
];
|
|
4337
4638
|
}
|
|
4338
4639
|
print(await new ErdoClient().recordPageFeedback(items));
|
|
4339
4640
|
} catch (e) {
|
|
@@ -4579,7 +4880,7 @@ reviewsCmd.command("decide <id>").description(
|
|
|
4579
4880
|
opts.apply ? "apply" : null,
|
|
4580
4881
|
opts.resolve ? "resolve" : null,
|
|
4581
4882
|
opts.reject ? "reject" : null,
|
|
4582
|
-
opts.snooze
|
|
4883
|
+
opts.snooze === void 0 ? null : "snooze"
|
|
4583
4884
|
].filter(Boolean);
|
|
4584
4885
|
if (chosen.length !== 1) {
|
|
4585
4886
|
fail(new Error("provide exactly one of --apply, --resolve, --reject, or --snooze"));
|
|
@@ -4599,6 +4900,11 @@ reviewsCmd.command("decide <id>").description(
|
|
|
4599
4900
|
}
|
|
4600
4901
|
);
|
|
4601
4902
|
var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
|
|
4903
|
+
function publicFlag(opts) {
|
|
4904
|
+
if (opts.public) return true;
|
|
4905
|
+
if (opts.private) return false;
|
|
4906
|
+
return void 0;
|
|
4907
|
+
}
|
|
4602
4908
|
function grantList(v) {
|
|
4603
4909
|
if (v === void 0) return void 0;
|
|
4604
4910
|
if (v.trim() === "[]") return [];
|
|
@@ -4674,7 +4980,9 @@ pagesCmd.command("update <id>").description(
|
|
|
4674
4980
|
writable_dataset_slugs: grantList(opts.writableDatasets),
|
|
4675
4981
|
kv_slugs: grantList(opts.kv),
|
|
4676
4982
|
writable_kv_slugs: grantList(opts.writableKv),
|
|
4677
|
-
|
|
4983
|
+
// --public and --private are mutually exclusive flags; neither set
|
|
4984
|
+
// leaves the server's default standing.
|
|
4985
|
+
public: publicFlag(opts),
|
|
4678
4986
|
request_review: !!opts.requestReview
|
|
4679
4987
|
});
|
|
4680
4988
|
console.log(`${res.id} ${res.public_url || res.url}`);
|
|
@@ -5039,11 +5347,13 @@ sendingDomainsCmd.command("remove <domain>").description(
|
|
|
5039
5347
|
});
|
|
5040
5348
|
emailCmd.command("received").description(
|
|
5041
5349
|
"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."
|
|
5042
|
-
).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) => {
|
|
5350
|
+
).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) => {
|
|
5043
5351
|
try {
|
|
5044
5352
|
const res = await new ErdoClient().listReceivedEmails({
|
|
5045
5353
|
domain: opts.domain,
|
|
5046
|
-
limit: opts.limit ? Number(opts.limit) : void 0
|
|
5354
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
5355
|
+
leadRef: opts.leadRef,
|
|
5356
|
+
dataset: opts.dataset
|
|
5047
5357
|
});
|
|
5048
5358
|
if (opts.json) {
|
|
5049
5359
|
print(res);
|
|
@@ -5060,12 +5370,17 @@ emailCmd.command("received").description(
|
|
|
5060
5370
|
`);
|
|
5061
5371
|
for (const m of emails) {
|
|
5062
5372
|
const who = m.from_name ? `${m.from_name} <${m.from_email}>` : m.from_email;
|
|
5063
|
-
|
|
5373
|
+
const side = m.direction ? ` [${m.direction}${m.lead_email ? ` \xB7 lead ${m.lead_email}` : ""}]` : "";
|
|
5374
|
+
console.log(`${m.received_at} ${who} ${m.subject || "(no subject)"}${side}`);
|
|
5064
5375
|
const preview = (m.text_preview || "").replace(/\s+/g, " ").trim();
|
|
5065
5376
|
if (preview) console.log(` ${preview.length > 160 ? `${preview.slice(0, 160)}\u2026` : preview}`);
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5377
|
+
if (m.relay_skip_reason) {
|
|
5378
|
+
console.log(` kept in Erdo, not relayed (${m.relay_skip_reason})`);
|
|
5379
|
+
} else {
|
|
5380
|
+
console.log(
|
|
5381
|
+
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"
|
|
5382
|
+
);
|
|
5383
|
+
}
|
|
5069
5384
|
}
|
|
5070
5385
|
} catch (e) {
|
|
5071
5386
|
fail(e);
|
|
@@ -5225,12 +5540,14 @@ function leadReferenceLabel(reference) {
|
|
|
5225
5540
|
}
|
|
5226
5541
|
var LEAD_FILTER_HELP = "only this lead's conversations \u2014 its canonical lead id (UUID) or its 22-character lead reference";
|
|
5227
5542
|
var voiceCallsCmd = voiceCmd.command("calls").description("Inbound and outbound phone call records");
|
|
5228
|
-
voiceCallsCmd.command("list").description("List the organization's phone calls, newest first").option("--agent <slug>", "only calls held by this voice agent, by slug").option("--direction <direction>", "only 'inbound' (calls to the agent's number) or 'outbound'").option("--lead <lead>", LEAD_FILTER_HELP).option("--limit <n>", "page size (default 25, max 100)").option("--offset <n>", "rows to skip").option("--cursor <cursor>", "next_cursor from the preceding page (stable paging)").option("--json", "print the raw JSON result instead of a table").action(
|
|
5543
|
+
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("--since <rfc3339>", "only calls that started at or after this time (inclusive)").option("--until <rfc3339>", "only calls that started before this time (exclusive)").option("--lead <lead>", LEAD_FILTER_HELP).option("--limit <n>", "page size (default 25, max 100)").option("--offset <n>", "rows to skip").option("--cursor <cursor>", "next_cursor from the preceding page (stable paging)").option("--json", "print the raw JSON result instead of a table").action(
|
|
5229
5544
|
async (opts) => {
|
|
5230
5545
|
try {
|
|
5231
5546
|
const res = await new ErdoClient().listVoiceCalls({
|
|
5232
5547
|
agent: opts.agent,
|
|
5233
5548
|
direction: opts.direction,
|
|
5549
|
+
since: opts.since,
|
|
5550
|
+
until: opts.until,
|
|
5234
5551
|
canonical_lead_id: opts.lead,
|
|
5235
5552
|
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
5236
5553
|
offset: opts.offset ? Number(opts.offset) : void 0,
|
|
@@ -5242,7 +5559,14 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5242
5559
|
}
|
|
5243
5560
|
const calls = res.conversations ?? [];
|
|
5244
5561
|
if (calls.length === 0) {
|
|
5245
|
-
|
|
5562
|
+
if (res.total > 0) {
|
|
5563
|
+
process.stderr.write(
|
|
5564
|
+
`showing 0 of ${res.total} call(s) \u2014 offset/cursor is past the last one
|
|
5565
|
+
`
|
|
5566
|
+
);
|
|
5567
|
+
} else {
|
|
5568
|
+
console.log("No calls match those filters.");
|
|
5569
|
+
}
|
|
5246
5570
|
return;
|
|
5247
5571
|
}
|
|
5248
5572
|
printAlignedTable(
|
|
@@ -5254,6 +5578,8 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5254
5578
|
"status",
|
|
5255
5579
|
"secs",
|
|
5256
5580
|
"transcript",
|
|
5581
|
+
"placed by",
|
|
5582
|
+
"dial",
|
|
5257
5583
|
"contact",
|
|
5258
5584
|
"lead",
|
|
5259
5585
|
"lead ref",
|
|
@@ -5268,6 +5594,10 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5268
5594
|
call.status,
|
|
5269
5595
|
call.duration_seconds,
|
|
5270
5596
|
call.has_transcript ? "yes" : "no",
|
|
5597
|
+
// "-" rather than blank: a call the AI agent placed has no person
|
|
5598
|
+
// behind it, which is a fact, not a missing value.
|
|
5599
|
+
call.placed_by_name || "-",
|
|
5600
|
+
call.dial_outcome || "-",
|
|
5271
5601
|
contactLabel(call.contact),
|
|
5272
5602
|
leadStatusLabel(call.lead_status),
|
|
5273
5603
|
leadReferenceLabel(call.lead_reference),
|
|
@@ -5275,7 +5605,7 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5275
5605
|
call.transcript_summary
|
|
5276
5606
|
])
|
|
5277
5607
|
);
|
|
5278
|
-
process.stderr.write(`showing ${calls.length} call(s)
|
|
5608
|
+
process.stderr.write(`showing ${calls.length} of ${res.total} call(s)
|
|
5279
5609
|
`);
|
|
5280
5610
|
if (res.next_cursor) {
|
|
5281
5611
|
process.stderr.write(`more available \u2014 re-run with --cursor ${res.next_cursor}
|
|
@@ -5287,7 +5617,7 @@ voiceCallsCmd.command("list").description("List the organization's phone calls,
|
|
|
5287
5617
|
}
|
|
5288
5618
|
);
|
|
5289
5619
|
voiceCallsCmd.command("get <callID>").description(
|
|
5290
|
-
"Read one phone call in full: transcript, summary, per-turn LLM metrics, and the lead it produced (contact, lead_status, lead_saved_at, canonical_lead_id, lead_reference)"
|
|
5620
|
+
"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)"
|
|
5291
5621
|
).action(async (callID) => {
|
|
5292
5622
|
try {
|
|
5293
5623
|
print(await new ErdoClient().getVoiceCall(callID));
|
|
@@ -5314,7 +5644,14 @@ widgetConversationsCmd.command("list").description("List the organization's widg
|
|
|
5314
5644
|
}
|
|
5315
5645
|
const conversations = res.conversations ?? [];
|
|
5316
5646
|
if (conversations.length === 0) {
|
|
5317
|
-
|
|
5647
|
+
if (res.total > 0) {
|
|
5648
|
+
process.stderr.write(
|
|
5649
|
+
`showing 0 of ${res.total} conversation(s) \u2014 offset/cursor is past the last one
|
|
5650
|
+
`
|
|
5651
|
+
);
|
|
5652
|
+
} else {
|
|
5653
|
+
console.log("No conversations match those filters.");
|
|
5654
|
+
}
|
|
5318
5655
|
return;
|
|
5319
5656
|
}
|
|
5320
5657
|
printAlignedTable(
|
|
@@ -5519,12 +5856,14 @@ voiceNumbersCmd.command("list").description("List the organization's numbers wit
|
|
|
5519
5856
|
if (numbers.length === 0) {
|
|
5520
5857
|
console.log("This organization holds no phone numbers.");
|
|
5521
5858
|
} else {
|
|
5859
|
+
const callable = new Set(res.person_call_numbers ?? []);
|
|
5522
5860
|
printAlignedTable(
|
|
5523
|
-
["number", "agent", "kind", "status", "waiting on", "reason"],
|
|
5861
|
+
["number", "agent", "kind", "calls", "status", "waiting on", "reason"],
|
|
5524
5862
|
numbers.map((n) => [
|
|
5525
5863
|
n.address,
|
|
5526
5864
|
n.agent_ref ?? "",
|
|
5527
5865
|
n.sender_kind,
|
|
5866
|
+
callable.has(n.address) ? "yes" : "",
|
|
5528
5867
|
n.status,
|
|
5529
5868
|
n.next_step ?? "",
|
|
5530
5869
|
n.reason ?? ""
|
|
@@ -5609,6 +5948,28 @@ voiceAgentsCmd.command("sms-replies <slug>").description("Turn a voice agent's a
|
|
|
5609
5948
|
fail(e);
|
|
5610
5949
|
}
|
|
5611
5950
|
});
|
|
5951
|
+
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) => {
|
|
5952
|
+
if (Boolean(opts.on) === Boolean(opts.off)) {
|
|
5953
|
+
fail(new Error("say which way: pass --on or --off (exactly one)"));
|
|
5954
|
+
}
|
|
5955
|
+
try {
|
|
5956
|
+
const agent = await new ErdoClient().setVoiceAgentWhatsAppReplies(slug, Boolean(opts.on));
|
|
5957
|
+
console.log(
|
|
5958
|
+
`${agent.slug} ${agent.whatsapp_replies_enabled ? "on" : "off"} ${agent.phone_number ?? "-"} ${agent.name}`
|
|
5959
|
+
);
|
|
5960
|
+
if (!agent.phone_number) {
|
|
5961
|
+
process.stderr.write(
|
|
5962
|
+
"this agent has no phone number yet \u2014 the setting is stored and applies once it gets one\n"
|
|
5963
|
+
);
|
|
5964
|
+
} else {
|
|
5965
|
+
process.stderr.write(
|
|
5966
|
+
"the number also has to be set up as a WhatsApp sender before any reply can go out\n"
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
} catch (e) {
|
|
5970
|
+
fail(e);
|
|
5971
|
+
}
|
|
5972
|
+
});
|
|
5612
5973
|
var datasetsCmd = program.command("datasets").description("Datasets");
|
|
5613
5974
|
datasetsCmd.command("list").description("List datasets").option(
|
|
5614
5975
|
"--class <class>",
|
|
@@ -6052,8 +6413,10 @@ refreshCmd.command("set <slug>").description(
|
|
|
6052
6413
|
const slugs = opts.contextDatasets.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6053
6414
|
if (slugs.length) body.context_dataset_slugs = slugs;
|
|
6054
6415
|
}
|
|
6055
|
-
if (
|
|
6056
|
-
if (opts.staleAfter !== void 0)
|
|
6416
|
+
if (opts.onView !== void 0) body.refresh_on_view_enabled = opts.onView;
|
|
6417
|
+
if (opts.staleAfter !== void 0) {
|
|
6418
|
+
body.refresh_on_view_stale_after_seconds = opts.staleAfter;
|
|
6419
|
+
}
|
|
6057
6420
|
if (opts.debounce !== void 0) body.refresh_on_view_debounce_seconds = opts.debounce;
|
|
6058
6421
|
if (opts.liveWriteMode) body.live_write_mode = opts.liveWriteMode;
|
|
6059
6422
|
if (opts.payloadSchema) {
|
|
@@ -6502,7 +6865,7 @@ List tables with: erdo integrations tables ${app} <schema>`);
|
|
|
6502
6865
|
return;
|
|
6503
6866
|
}
|
|
6504
6867
|
for (const t of res.tables ?? []) {
|
|
6505
|
-
const rows = t.estimated_row_count
|
|
6868
|
+
const rows = t.estimated_row_count == null ? "" : ` ~${t.estimated_row_count} rows`;
|
|
6506
6869
|
console.log(`${t.schema_name}.${t.table_name} ${t.columns.length} columns${rows}`);
|
|
6507
6870
|
}
|
|
6508
6871
|
} catch (e) {
|