@erdoai/cli 0.60.0 → 0.61.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 +238 -1
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -310,6 +310,53 @@ var ErdoClient = class {
310
310
  { target_org: targetOrg }
311
311
  );
312
312
  }
313
+ // --- Email sending domains: the address outbound agent mail is sent FROM ---
314
+ // Registering one is DNS only: the response carries the records the customer
315
+ // creates at their own provider, and nothing leaves the domain until they exist
316
+ // and Erdo has verified them — sends keep falling back to Erdo's own address in
317
+ // the meantime, so status is the thing to read, not the fact of registration.
318
+ // An org has one sending domain; to move, remove the current one first.
319
+ listSendingDomains() {
320
+ return this.request("GET", "/v1/sending-domains");
321
+ }
322
+ getSendingDomain(domain) {
323
+ return this.request(
324
+ "GET",
325
+ `/v1/sending-domains/${encodeURIComponent(domain)}`
326
+ );
327
+ }
328
+ createSendingDomain(input) {
329
+ return this.request("POST", "/v1/sending-domains", input);
330
+ }
331
+ // Absent fields are left unchanged; a field passed empty is set empty. The
332
+ // domain rides in the path, so the body carries only what is changing.
333
+ updateSendingDomain(domain, input) {
334
+ return this.request(
335
+ "PATCH",
336
+ `/v1/sending-domains/${encodeURIComponent(domain)}`,
337
+ input
338
+ );
339
+ }
340
+ deleteSendingDomain(domain) {
341
+ return this.request(
342
+ "DELETE",
343
+ `/v1/sending-domains/${encodeURIComponent(domain)}`
344
+ );
345
+ }
346
+ // The mail that arrived AT the org's sending domain, newest first — the other
347
+ // half of the outreach the send path produces. `domain` can be left off: an org
348
+ // has one sending domain and the read resolves it. Needs only org membership,
349
+ // unlike the registration endpoints above.
350
+ listReceivedEmails(params) {
351
+ const q = new URLSearchParams();
352
+ if (params?.domain) q.set("domain", params.domain);
353
+ if (params?.limit) q.set("limit", String(params.limit));
354
+ const qs = q.toString();
355
+ return this.request(
356
+ "GET",
357
+ `/v1/received-emails${qs ? `?${qs}` : ""}`
358
+ );
359
+ }
313
360
  // Run a read-only HogQL query against the org's page-analytics events. Rows are
314
361
  // positional per columns; enabled:false means page analytics is off for the org
315
362
  // (not zero traffic). A rejected query surfaces PostHog's message as the error.
@@ -726,7 +773,12 @@ var ErdoClient = class {
726
773
  const qs = q.toString();
727
774
  return this.request("GET", `/v1/datasets${qs ? `?${qs}` : ""}`);
728
775
  }
729
- // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
776
+ // The org's dataset vocabulary: every purpose in use, its description, and
777
+ // the dataset that established it (the canonical write target for that role).
778
+ listDatasetPurposes() {
779
+ return this.request("GET", `/v1/datasets-purposes`);
780
+ }
781
+ // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
730
782
  // `query` made every `erdo datasets query` fail with "question is required".
731
783
  //
732
784
  // `rows` carries the values in `columns` order — the answer to the question —
@@ -3668,6 +3720,174 @@ domainsCmd.command("transfer <domain> <targetOrg>").description("Move a domain t
3668
3720
  fail(e);
3669
3721
  }
3670
3722
  });
3723
+ var emailCmd = program.command("email").description("The domain Erdo sends your outbound agent email from, and the mail that comes back");
3724
+ var sendingDomainsCmd = emailCmd.command("domains").description("Sending domains \u2014 the verified domain outreach is sent from (one per org)");
3725
+ function printSendingDomain(d) {
3726
+ console.log(`${d.domain} ${d.status}${d.error_reason ? ` ${d.error_reason}` : ""}`);
3727
+ console.log(`from: ${d.from_email}${d.from_name ? ` (${d.from_name})` : ""}`);
3728
+ console.log(
3729
+ `receiving: ${d.receiving_enabled ? "on" : "off"}${d.forward_to_email ? `, forwards to ${d.forward_to_email}` : ""}`
3730
+ );
3731
+ if (d.status !== "active") {
3732
+ console.error(
3733
+ "Not active yet \u2014 sends still go out from Erdo's own address until every record below is live and verified (re-checked about every ten minutes)."
3734
+ );
3735
+ }
3736
+ if (d.existing_mx_detected) {
3737
+ console.error(
3738
+ "This domain already routes mail to a mailbox provider, so it was registered sending-only and no MX record was issued \u2014 your existing email is untouched. To receive replies through Erdo, remove it and register a subdomain instead."
3739
+ );
3740
+ } else if (!d.registered_receiving) {
3741
+ console.error(
3742
+ "Registered sending-only: receiving cannot be turned on here. Remove the domain and register it (or a subdomain) again to receive replies."
3743
+ );
3744
+ }
3745
+ const records = d.dns_records ?? [];
3746
+ if (records.length > 0) {
3747
+ console.error("Create these records at the domain's DNS provider:");
3748
+ for (const r of records) {
3749
+ console.log(`${r.type} ${r.name} ${r.value}`);
3750
+ }
3751
+ }
3752
+ }
3753
+ sendingDomainsCmd.command("list").description("List the org's sending domains with their verification status").option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
3754
+ try {
3755
+ const res = await new ErdoClient().listSendingDomains();
3756
+ if (opts.json) {
3757
+ print(res);
3758
+ return;
3759
+ }
3760
+ const domains = res.sending_domains ?? [];
3761
+ if (domains.length === 0) {
3762
+ console.log(
3763
+ "No sending domain \u2014 this org's agent email is sent from Erdo's own address, which is a working default rather than a misconfiguration. Register one with: erdo email domains add <domain>"
3764
+ );
3765
+ return;
3766
+ }
3767
+ printAlignedTable(
3768
+ ["domain", "status", "from", "receiving", "forwards to", "checked"],
3769
+ domains.map((d) => [
3770
+ d.domain,
3771
+ d.status,
3772
+ d.from_name ? `${d.from_email} (${d.from_name})` : d.from_email,
3773
+ d.receiving_enabled ? "on" : "off",
3774
+ d.forward_to_email ?? "",
3775
+ d.last_checked_at ?? "never"
3776
+ ])
3777
+ );
3778
+ } catch (e) {
3779
+ fail(e);
3780
+ }
3781
+ });
3782
+ sendingDomainsCmd.command("get <domain>").description(
3783
+ "Read one sending domain \u2014 its status, the address it sends as, and the DNS records that must exist. The command to run while waiting for DNS to propagate: error_reason names the record that did not check out."
3784
+ ).option("--json", "print the raw JSON result instead of the status and records").action(async (domain, opts) => {
3785
+ try {
3786
+ const d = await new ErdoClient().getSendingDomain(domain);
3787
+ if (opts.json) {
3788
+ print(d);
3789
+ return;
3790
+ }
3791
+ printSendingDomain(d);
3792
+ } catch (e) {
3793
+ fail(e);
3794
+ }
3795
+ });
3796
+ sendingDomainsCmd.command("add <domain>").description(
3797
+ "Register the domain outbound agent email should be sent from (an apex, acme.com, or a subdomain, hello.acme.com) and print the DNS records to create. Nothing sends from it until those records exist and verify."
3798
+ ).option("--from-name <name>", "display name on the From header, e.g. 'Acme Sales'").option("--from-local-part <part>", "the part before the @ (default: hello)").option(
3799
+ "--receiving <bool>",
3800
+ "accept replies back into Erdo (true/false; defaults on). Needs an MX record, which a name already routing mail elsewhere cannot have"
3801
+ ).option(
3802
+ "--forward-to <email>",
3803
+ "a real mailbox each received reply is copied to, e.g. sales@acme.com \u2014 required when the domain is sending-only"
3804
+ ).action(
3805
+ async (domain, opts) => {
3806
+ try {
3807
+ const d = await new ErdoClient().createSendingDomain({
3808
+ domain,
3809
+ from_name: opts.fromName,
3810
+ from_local_part: opts.fromLocalPart,
3811
+ receiving_enabled: opts.receiving === void 0 ? void 0 : parseStrictBoolean(opts.receiving),
3812
+ forward_to_email: opts.forwardTo
3813
+ });
3814
+ printSendingDomain(d);
3815
+ } catch (e) {
3816
+ fail(e);
3817
+ }
3818
+ }
3819
+ );
3820
+ sendingDomainsCmd.command("update <domain>").description(
3821
+ "Edit a sending domain's identity \u2014 only the flags you pass change, and they take effect on the next send"
3822
+ ).option("--from-name <name>", "new display name on the From header").option("--from-local-part <part>", "new part before the @, e.g. 'sales'").option(
3823
+ "--forward-to <email>",
3824
+ 'new mailbox received replies are copied to (pass "" to clear, which a sending-only domain refuses)'
3825
+ ).option(
3826
+ "--receiving <bool>",
3827
+ "accept replies back into Erdo (true/false). Can only be turned on for a domain REGISTERED with receiving"
3828
+ ).action(
3829
+ async (domain, opts) => {
3830
+ try {
3831
+ const body = {
3832
+ from_name: opts.fromName,
3833
+ from_local_part: opts.fromLocalPart,
3834
+ forward_to_email: opts.forwardTo,
3835
+ receiving_enabled: opts.receiving === void 0 ? void 0 : parseStrictBoolean(opts.receiving)
3836
+ };
3837
+ if (Object.values(body).every((v) => v === void 0)) {
3838
+ fail(new Error("nothing to update \u2014 pass at least one field to change"));
3839
+ return;
3840
+ }
3841
+ printSendingDomain(await new ErdoClient().updateSendingDomain(domain, body));
3842
+ } catch (e) {
3843
+ fail(e);
3844
+ }
3845
+ }
3846
+ );
3847
+ sendingDomainsCmd.command("remove <domain>").description(
3848
+ "Remove a sending-domain registration \u2014 outbound agent email reverts to Erdo's own address on the next send and replies to the old address stop reaching Erdo. The DNS records are in your own DNS and are left alone."
3849
+ ).action(async (domain) => {
3850
+ try {
3851
+ const res = await new ErdoClient().deleteSendingDomain(domain);
3852
+ console.log(`removed: ${res.domain}`);
3853
+ } catch (e) {
3854
+ fail(e);
3855
+ }
3856
+ });
3857
+ emailCmd.command("received").description(
3858
+ "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."
3859
+ ).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) => {
3860
+ try {
3861
+ const res = await new ErdoClient().listReceivedEmails({
3862
+ domain: opts.domain,
3863
+ limit: opts.limit ? Number(opts.limit) : void 0
3864
+ });
3865
+ if (opts.json) {
3866
+ print(res);
3867
+ return;
3868
+ }
3869
+ const emails = res.emails ?? [];
3870
+ if (emails.length === 0) {
3871
+ console.log(
3872
+ `No mail has arrived at ${res.domain}. Replies only come back for a domain registered with receiving on and verified active \u2014 a sending-only domain sends them straight to its nominated mailbox, which is the configuration working rather than a fault.`
3873
+ );
3874
+ return;
3875
+ }
3876
+ process.stderr.write(`${res.domain} \u2014 ${emails.length} message(s), newest first
3877
+ `);
3878
+ for (const m of emails) {
3879
+ const who = m.from_name ? `${m.from_name} <${m.from_email}>` : m.from_email;
3880
+ console.log(`${m.received_at} ${who} ${m.subject || "(no subject)"}`);
3881
+ const preview = (m.text_preview || "").replace(/\s+/g, " ").trim();
3882
+ if (preview) console.log(` ${preview.length > 160 ? `${preview.slice(0, 160)}\u2026` : preview}`);
3883
+ console.log(
3884
+ 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"
3885
+ );
3886
+ }
3887
+ } catch (e) {
3888
+ fail(e);
3889
+ }
3890
+ });
3671
3891
  var datasetsCmd = program.command("datasets").description("Datasets");
3672
3892
  datasetsCmd.command("list").description("List datasets").option(
3673
3893
  "--class <class>",
@@ -3695,6 +3915,23 @@ datasetsCmd.command("list").description("List datasets").option(
3695
3915
  fail(e);
3696
3916
  }
3697
3917
  });
3918
+ datasetsCmd.command("purposes").description(
3919
+ "List the org's dataset purposes \u2014 the vocabulary of dataset roles in use (e.g. leads, page events). The established_by slug is the canonical dataset for that role: write there rather than creating a sibling (one purpose = one dataset per org)."
3920
+ ).action(async () => {
3921
+ try {
3922
+ const { purposes } = await new ErdoClient().listDatasetPurposes();
3923
+ if (purposes.length === 0) {
3924
+ console.error("(no dataset purposes established yet)");
3925
+ return;
3926
+ }
3927
+ for (const p of purposes)
3928
+ console.log(
3929
+ `${p.purpose} ${p.established_by_dataset_slug ?? p.established_by_dataset_id} ${p.dataset_count} dataset${p.dataset_count === 1 ? "" : "s"} ${p.description}`
3930
+ );
3931
+ } catch (e) {
3932
+ fail(e);
3933
+ }
3934
+ });
3698
3935
  datasetsCmd.command("query <slug> <question>").description(
3699
3936
  "Ask a natural-language question of a dataset \u2014 Erdo writes and runs the SQL, and answers with that SQL alongside the values. It runs an agent, so it is slower and two identical questions can produce two different queries: for a deterministic or scripted read, write the SQL yourself with `datasets fetch --sql`."
3700
3937
  ).action(async (slug, question) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.60.0",
4
- "description": "Erdo CLI drive datasets, pages, and evals from the terminal or CI",
3
+ "version": "0.61.0",
4
+ "description": "Erdo CLI \u2014 drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "erdo": "dist/index.js"
@@ -52,4 +52,4 @@
52
52
  "overrides": {
53
53
  "esbuild": "^0.28.1"
54
54
  }
55
- }
55
+ }