@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.
Files changed (2) hide show
  1. package/dist/index.js +424 -61
  2. 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) => v === null || v === void 0 ? "" : typeof v === "object" ? JSON.stringify(v) : String(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
- const status = r.agent_error ? "ERROR" : r.passed ? "PASS" : "FAIL";
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("--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(
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
- console.log(
2524
- c.already_provisioned ? `Google Ads container already existed for ${c.org_slug}` : c.adopted ? `Adopted Google Ads account ${c.customer_id} as the container for ${c.org_slug}` : `Created Google Ads container for ${c.org_slug}`
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] += "," + raw;
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 = `${" " + stat}`.padEnd(34);
3533
+ let line = ` ${stat}`.padEnd(34);
3383
3534
  for (const s of res.summary) {
3384
- const v = stat === "score" ? s.avg_score.toFixed(2) : stat === "pass" ? `${Math.round(s.pass_rate * 100)}%` : stat === "cost" ? `$${(s.avg_cost_millicents / 1e5).toFixed(3)}/case` : `${(s.avg_duration_ms / 1e3).toFixed(0)}s`;
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, b) => a === "skill" ? -1 : b === "skill" ? 1 : a.localeCompare(b)
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
- if (detail.lineage.supersedes_slug || detail.lineage.superseded_by_slugs?.length) {
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 (detail.lineage.supersedes_slug) console.log(` replaced ${detail.lineage.supersedes_slug}`);
3901
- for (const slug of detail.lineage.superseded_by_slugs ?? []) {
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 !== void 0) {
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
- 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("-n, --limit <n>", "max items", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).action(
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
- print(
4218
- await new ErdoClient().listAttentionItems({
4219
- statuses,
4220
- engine_actions_only: opts.engineActions,
4221
- item_id: opts.item,
4222
- limit: opts.limit,
4223
- offset: opts.offset
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 !== void 0) {
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 !== void 0 ? "snooze" : null
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
- public: opts.public ? true : opts.private ? false : void 0,
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
- console.log(`${m.received_at} ${who} ${m.subject || "(no subject)"}`);
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
- console.log(
5067
- m.forwarded_at ? ` forwarded ${m.forwarded_at}` : " not forwarded \u2014 this one is in Erdo only, so nobody on your side has necessarily seen it"
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
- console.log("No calls match those filters.");
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
- console.log("No conversations match those filters.");
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 (typeof opts.onView === "boolean") body.refresh_on_view_enabled = opts.onView;
6056
- if (opts.staleAfter !== void 0) body.refresh_on_view_stale_after_seconds = opts.staleAfter;
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 != null ? ` ~${t.estimated_row_count} rows` : "";
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.90.0",
3
+ "version": "0.98.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {