@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.
- package/dist/index.js +238 -1
- 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
|
|
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.
|
|
4
|
-
"description": "Erdo CLI
|
|
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
|
+
}
|