@erdoai/cli 0.67.0 → 0.68.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 +220 -6
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -331,8 +331,30 @@ var ErdoClient = class {
331
331
  listCustomDomains() {
332
332
  return this.request("GET", "/v1/custom-domains");
333
333
  }
334
- createCustomDomain(domain) {
335
- return this.request("POST", "/v1/custom-domains", { domain });
334
+ // `managerDefault` registers the domain and points the orgs this one manages at
335
+ // it in one step, for a manager setting up its very first branded hostname. The
336
+ // key is left out of the body when false so an ordinary registration sends what
337
+ // it always sent; changing the flag on a domain that is already live goes
338
+ // through setCustomDomainManagerDefault, which does not touch the certificate.
339
+ createCustomDomain(domain, managerDefault) {
340
+ return this.request(
341
+ "POST",
342
+ "/v1/custom-domains",
343
+ managerDefault ? { domain, manager_default: true } : { domain }
344
+ );
345
+ }
346
+ // Marks a domain the org already owns as the one its managed orgs serve their
347
+ // pages from, or clears it. At most one domain per org carries the flag, so
348
+ // promoting a second demotes the first in the same write — the managed orgs
349
+ // never see a moment with no default and fall back to platform URLs. Only a
350
+ // manager account with at least one client org can set it; clearing is always
351
+ // allowed, so an org that has since let its last client go can still tidy up.
352
+ setCustomDomainManagerDefault(domain, managerDefault) {
353
+ return this.request(
354
+ "PATCH",
355
+ `/v1/custom-domains/${encodeURIComponent(domain)}/manager-default`,
356
+ { manager_default: managerDefault }
357
+ );
336
358
  }
337
359
  deleteCustomDomain(domain) {
338
360
  return this.request(
@@ -888,6 +910,69 @@ var ErdoClient = class {
888
910
  listDatasetRevisions(slug) {
889
911
  return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
890
912
  }
913
+ // --- bounded outreach ---
914
+ putOutreachBatch(batch, body) {
915
+ return this.request(
916
+ "PUT",
917
+ `/v1/outreach-batches/${encodeURIComponent(batch)}`,
918
+ body
919
+ );
920
+ }
921
+ listOutreachBatches(opts = {}) {
922
+ const q = new URLSearchParams();
923
+ if (opts.limit !== void 0) q.set("limit", String(opts.limit));
924
+ if (opts.cursor) q.set("cursor", opts.cursor);
925
+ if (opts.initiative_ref) q.set("initiative_ref", opts.initiative_ref);
926
+ const qs = q.toString();
927
+ return this.request(
928
+ "GET",
929
+ `/v1/outreach-batches${qs ? `?${qs}` : ""}`
930
+ );
931
+ }
932
+ getOutreachBatch(batch) {
933
+ return this.request(
934
+ "GET",
935
+ `/v1/outreach-batches/${encodeURIComponent(batch)}`
936
+ );
937
+ }
938
+ listOutreachBatchRecipients(batch, opts = {}) {
939
+ const q = new URLSearchParams();
940
+ if (opts.limit !== void 0) q.set("limit", String(opts.limit));
941
+ if (opts.cursor) q.set("cursor", opts.cursor);
942
+ const qs = q.toString();
943
+ return this.request(
944
+ "GET",
945
+ `/v1/outreach-batches/${encodeURIComponent(batch)}/recipients${qs ? `?${qs}` : ""}`
946
+ );
947
+ }
948
+ actOnOutreachBatch(batch, body) {
949
+ return this.request(
950
+ "POST",
951
+ `/v1/outreach-batches/${encodeURIComponent(batch)}/actions`,
952
+ body
953
+ );
954
+ }
955
+ putOutreachConsent(body) {
956
+ return this.request("PUT", "/v1/outreach-consents", body);
957
+ }
958
+ listOutreachConsents(opts = {}) {
959
+ const q = new URLSearchParams();
960
+ if (opts.limit !== void 0) q.set("limit", String(opts.limit));
961
+ if (opts.cursor) q.set("cursor", opts.cursor);
962
+ const qs = q.toString();
963
+ return this.request(
964
+ "GET",
965
+ `/v1/outreach-consents${qs ? `?${qs}` : ""}`
966
+ );
967
+ }
968
+ getOutreachConsent(opts) {
969
+ const q = new URLSearchParams();
970
+ if (opts.grant_ref) q.set("grant_ref", opts.grant_ref);
971
+ if (opts.recipient_ref) q.set("recipient_ref", opts.recipient_ref);
972
+ if (opts.phone_raw) q.set("phone_raw", opts.phone_raw);
973
+ if (opts.default_country) q.set("default_country", opts.default_country);
974
+ return this.request("GET", `/v1/outreach-consents?${q.toString()}`);
975
+ }
891
976
  uploadDatasetFile(body) {
892
977
  return this.request("POST", "/v1/datasets-upload", body);
893
978
  }
@@ -1534,6 +1619,25 @@ function timedOutMessage(threadID) {
1534
1619
  function print(value) {
1535
1620
  console.log(JSON.stringify(value, null, 2));
1536
1621
  }
1622
+ function readJSONObject(file, label) {
1623
+ let parsed;
1624
+ try {
1625
+ parsed = JSON.parse(readFileSync4(file, "utf8"));
1626
+ } catch (error) {
1627
+ throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`);
1628
+ }
1629
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
1630
+ throw new Error(`${label} must contain one JSON object`);
1631
+ }
1632
+ return parsed;
1633
+ }
1634
+ function requireOutreachProject() {
1635
+ if (!process.env.ERDO_PROJECT?.trim()) {
1636
+ throw new Error(
1637
+ "outreach commands require a project: pass --project <uuid> (and --org when the key can reach more than one organization)"
1638
+ );
1639
+ }
1640
+ }
1537
1641
  function printPageReview(review) {
1538
1642
  if (!review) return;
1539
1643
  if (!review.reviewed) {
@@ -3924,17 +4028,21 @@ domainsCmd.command("list").description("List the org's custom domains with live
3924
4028
  }
3925
4029
  for (const d of domains) {
3926
4030
  const checked = d.last_checked_at ? `checked ${d.last_checked_at}` : "never checked";
4031
+ const managerDefault = d.manager_default ? " manager default" : "";
3927
4032
  const reason = d.error_reason ? ` ${d.error_reason}` : "";
3928
- console.log(`${d.domain} ${d.status} ${checked}${reason}`);
4033
+ console.log(`${d.domain} ${d.status} ${checked}${managerDefault}${reason}`);
3929
4034
  }
3930
4035
  } catch (e) {
3931
4036
  fail(e);
3932
4037
  }
3933
4038
  });
3934
- domainsCmd.command("add <domain>").description("Register a custom domain (a direct subdomain, e.g. pages.acme.com) and print the DNS records to create").action(async (domain) => {
4039
+ domainsCmd.command("add <domain>").description("Register a custom domain (a direct subdomain, e.g. pages.acme.com) and print the DNS records to create").option(
4040
+ "--manager-default",
4041
+ "also point the orgs you manage at this domain, for a manager registering its first branded hostname"
4042
+ ).action(async (domain, opts) => {
3935
4043
  try {
3936
- const d = await new ErdoClient().createCustomDomain(domain);
3937
- console.log(`${d.domain}: ${d.status}`);
4044
+ const d = await new ErdoClient().createCustomDomain(domain, opts.managerDefault);
4045
+ console.log(`${d.domain}: ${d.status}${d.manager_default ? " (manager default)" : ""}`);
3938
4046
  if (d.dns_records.length > 0) {
3939
4047
  process.stderr.write("Create these records at the domain's DNS provider:\n");
3940
4048
  for (const r of d.dns_records) {
@@ -3945,6 +4053,20 @@ domainsCmd.command("add <domain>").description("Register a custom domain (a dire
3945
4053
  fail(e);
3946
4054
  }
3947
4055
  });
4056
+ domainsCmd.command("manager-default <domain>").description(
4057
+ "Make this the domain the orgs you manage serve their pages from when they have registered none of their own \u2014 a client with its own domain keeps it \u2014 or clear that with --unset"
4058
+ ).option("--unset", "clear the flag instead, returning the managed orgs to the platform pages host").action(async (domain, opts) => {
4059
+ try {
4060
+ const d = await new ErdoClient().setCustomDomainManagerDefault(domain, !opts.unset);
4061
+ if (d.manager_default) {
4062
+ console.log(`${d.domain} is now the default domain for the orgs you manage (status: ${d.status})`);
4063
+ } else {
4064
+ console.log(`${d.domain} is no longer the default domain for the orgs you manage`);
4065
+ }
4066
+ } catch (e) {
4067
+ fail(e);
4068
+ }
4069
+ });
3948
4070
  domainsCmd.command("remove <domain>").description("Remove a custom domain registration (stops it serving pages)").action(async (domain) => {
3949
4071
  try {
3950
4072
  const res = await new ErdoClient().deleteCustomDomain(domain);
@@ -4168,6 +4290,98 @@ sentEmailsCmd.command("list").description("List sent email, newest first, includ
4168
4290
  }
4169
4291
  }
4170
4292
  );
4293
+ var outreachCmd = program.command("outreach").description("Consent-gated SMS outreach to an explicit reviewed selection of up to 700 people");
4294
+ var outreachBatchesCmd = outreachCmd.command("batches").description("Prepare and control outreach batches");
4295
+ outreachBatchesCmd.command("put <batch>").description("Create one immutable draft preview from a JSON declaration; this never starts sending").requiredOption("-f, --file <path>", "JSON object containing the batch declaration and recipients").action(async (batch, opts) => {
4296
+ try {
4297
+ requireOutreachProject();
4298
+ print(await new ErdoClient().putOutreachBatch(batch, readJSONObject(opts.file, "--file")));
4299
+ } catch (e) {
4300
+ fail(e);
4301
+ }
4302
+ });
4303
+ outreachBatchesCmd.command("list").description("List outreach batches in the required project").option("-l, --limit <n>", "max batches to return", (v) => parseInt(v, 10)).option("--cursor <cursor>", "opaque next_cursor from the preceding page").option("--initiative <ref>", "return only this outreach initiative before pagination").action(async (opts) => {
4304
+ try {
4305
+ requireOutreachProject();
4306
+ print(await new ErdoClient().listOutreachBatches({
4307
+ limit: opts.limit,
4308
+ cursor: opts.cursor,
4309
+ initiative_ref: opts.initiative
4310
+ }));
4311
+ } catch (e) {
4312
+ fail(e);
4313
+ }
4314
+ });
4315
+ outreachBatchesCmd.command("get <batch>").description("Read a batch summary, or its paged recipient receipts").option("--recipients", "return recipient eligibility, delivery, and response receipts").option("-l, --limit <n>", "max recipients to return", (v) => parseInt(v, 10)).option("--cursor <cursor>", "opaque next_cursor from the preceding recipient page").action(async (batch, opts) => {
4316
+ try {
4317
+ requireOutreachProject();
4318
+ const client = new ErdoClient();
4319
+ print(
4320
+ opts.recipients ? await client.listOutreachBatchRecipients(batch, opts) : await client.getOutreachBatch(batch)
4321
+ );
4322
+ } catch (e) {
4323
+ fail(e);
4324
+ }
4325
+ });
4326
+ outreachBatchesCmd.command("act <batch> <action>").description("Apply a replay-safe arm, pause, resume, or cancel action").requiredOption("--action-ref <ref>", "stable idempotency reference for this operator action").option("--preview-revision <revision>", "arm only this reviewed preview revision").option("--eligible-count <n>", "arm only when this many recipients remain eligible", (v) => parseInt(v, 10)).option("--message-hash <hash>", "arm only the reviewed message hash").option("--source-snapshot <ref>", "pause, resume, or cancel only the rendered source snapshot").option("--batch-state <state>", "pause, resume, or cancel only the rendered batch state").option("--batch-updated-at <timestamp>", "pause, resume, or cancel only the rendered batch version").action(
4327
+ async (batch, action, opts) => {
4328
+ try {
4329
+ requireOutreachProject();
4330
+ print(
4331
+ await new ErdoClient().actOnOutreachBatch(batch, {
4332
+ action,
4333
+ action_ref: opts.actionRef,
4334
+ expected_preview_revision: opts.previewRevision,
4335
+ expected_eligible_count: opts.eligibleCount,
4336
+ expected_message_hash: opts.messageHash,
4337
+ expected_source_snapshot_ref: opts.sourceSnapshot,
4338
+ expected_batch_state: opts.batchState,
4339
+ expected_batch_updated_at: opts.batchUpdatedAt
4340
+ })
4341
+ );
4342
+ } catch (e) {
4343
+ fail(e);
4344
+ }
4345
+ }
4346
+ );
4347
+ var outreachConsentsCmd = outreachCmd.command("consents").description("Declare and read auditable SMS consent evidence");
4348
+ outreachConsentsCmd.command("put").description("Declare or revoke one consent grant from a JSON object").requiredOption("-f, --file <path>", "JSON object containing the consent declaration").action(async (opts) => {
4349
+ try {
4350
+ requireOutreachProject();
4351
+ print(await new ErdoClient().putOutreachConsent(readJSONObject(opts.file, "--file")));
4352
+ } catch (e) {
4353
+ fail(e);
4354
+ }
4355
+ });
4356
+ outreachConsentsCmd.command("list").description("List consent grants in the required project").option("-l, --limit <n>", "max grants to return", (v) => parseInt(v, 10)).option("--cursor <cursor>", "opaque next_cursor from the preceding page").action(async (opts) => {
4357
+ try {
4358
+ requireOutreachProject();
4359
+ print(await new ErdoClient().listOutreachConsents(opts));
4360
+ } catch (e) {
4361
+ fail(e);
4362
+ }
4363
+ });
4364
+ outreachConsentsCmd.command("get [grant-ref]").description("Read one consent grant by reference, or by recipient and phone").option("--recipient-ref <ref>", "stable lead identity when the grant reference is not known").option("--phone <phone>", "source phone recorded for the recipient").option("--country <code>", "ISO country hint used to normalize --phone").action(async (grantRef, opts) => {
4365
+ try {
4366
+ requireOutreachProject();
4367
+ const byGrant = Boolean(grantRef);
4368
+ const byRecipient = Boolean(opts.recipientRef || opts.phone || opts.country);
4369
+ if (byGrant === byRecipient) {
4370
+ throw new Error("consents get requires either <grant-ref> or all of --recipient-ref, --phone, and --country");
4371
+ }
4372
+ if (!byGrant && !(opts.recipientRef && opts.phone && opts.country)) {
4373
+ throw new Error("consents get by recipient requires --recipient-ref, --phone, and --country together");
4374
+ }
4375
+ print(await new ErdoClient().getOutreachConsent({
4376
+ grant_ref: grantRef,
4377
+ recipient_ref: opts.recipientRef,
4378
+ phone_raw: opts.phone,
4379
+ default_country: opts.country
4380
+ }));
4381
+ } catch (e) {
4382
+ fail(e);
4383
+ }
4384
+ });
4171
4385
  sentEmailsCmd.command("get <emailID>").description("Read one sent email, including exact bodies and delivery evidence").action(async (emailID) => {
4172
4386
  try {
4173
4387
  print(await new ErdoClient().getSentEmail(emailID));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.67.0",
3
+ "version": "0.68.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "scripts": {
36
36
  "build": "tsup src/index.ts --format esm --clean",
37
37
  "build:check": "tsc --noEmit",
38
- "test": "node --import tsx --test src/input.test.ts",
38
+ "test": "node --import tsx --test src/*.test.ts",
39
39
  "dev": "tsx src/index.ts",
40
40
  "postinstall": "node scripts/postinstall.mjs",
41
41
  "prepublishOnly": "npm run build"