@erdoai/cli 0.66.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 +232 -10
  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(
@@ -464,7 +486,7 @@ var ErdoClient = class {
464
486
  `/v1/evals/suites/${encodeURIComponent(slug)}/cases/${encodeURIComponent(caseName)}`
465
487
  );
466
488
  }
467
- runEvalSuite(slug, concurrency, commitSha, frontendCommitSha, backendCommitSha, source, modelOverride) {
489
+ runEvalSuite(slug, concurrency, commitSha, frontendCommitSha, backendCommitSha, source, modelOverride, rotateCases) {
468
490
  return this.request(
469
491
  "POST",
470
492
  `/v1/evals/suites/${encodeURIComponent(slug)}/run`,
@@ -474,7 +496,8 @@ var ErdoClient = class {
474
496
  frontend_commit_sha: frontendCommitSha,
475
497
  backend_commit_sha: backendCommitSha,
476
498
  source,
477
- model_override: modelOverride
499
+ model_override: modelOverride,
500
+ rotate_cases: rotateCases
478
501
  }
479
502
  );
480
503
  }
@@ -887,6 +910,69 @@ var ErdoClient = class {
887
910
  listDatasetRevisions(slug) {
888
911
  return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
889
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
+ }
890
976
  uploadDatasetFile(body) {
891
977
  return this.request("POST", "/v1/datasets-upload", body);
892
978
  }
@@ -1533,6 +1619,25 @@ function timedOutMessage(threadID) {
1533
1619
  function print(value) {
1534
1620
  console.log(JSON.stringify(value, null, 2));
1535
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
+ }
1536
1641
  function printPageReview(review) {
1537
1642
  if (!review) return;
1538
1643
  if (!review.reviewed) {
@@ -2551,7 +2656,13 @@ evalCmd.command("create <name>").description("Create a suite (in the active org)
2551
2656
  }
2552
2657
  }
2553
2658
  );
2554
- evalCmd.command("run <slug>").description("Run a suite; --watch polls until it completes").option("-w, --watch", "poll until the run finishes and print results").option("-c, --concurrency <n>", "parallel cases", (v) => parseInt(v, 10)).option("--commit-sha <sha>", "full 40-character Git commit SHA to associate with the run").option("--frontend-commit-sha <sha>", "frontend revision in the evaluated production snapshot").option("--backend-commit-sha <sha>", "backend revision in the evaluated production snapshot").option("--source <source>", "staff-only run source: post_deploy, nightly, or rollback").option("--model <model>", "pin the suite's agent to a model for this run (e.g. glm-5.3) \u2014 for model comparisons").action(async (slug, opts) => {
2659
+ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it completes").option("-w, --watch", "poll until the run finishes and print results").option("-c, --concurrency <n>", "parallel cases", (v) => parseInt(v, 10)).option("--commit-sha <sha>", "full 40-character Git commit SHA to associate with the run").option("--frontend-commit-sha <sha>", "frontend revision in the evaluated production snapshot").option("--backend-commit-sha <sha>", "backend revision in the evaluated production snapshot").option("--source <source>", "staff-only run source: post_deploy, nightly, or rollback").option("--model <model>", "pin the suite's agent to a model for this run (e.g. glm-5.3) \u2014 for model comparisons").option("--rotate-cases <n>", "run a rotating subset of n cases (deterministic per suite + UTC day) instead of all cases", (v) => {
2660
+ const parsed = parseInt(v, 10);
2661
+ if (isNaN(parsed) || parsed < 0) {
2662
+ throw new Error(`--rotate-cases must be a non-negative integer, got "${v}"`);
2663
+ }
2664
+ return parsed;
2665
+ }).action(async (slug, opts) => {
2555
2666
  try {
2556
2667
  const api = new ErdoClient();
2557
2668
  const { suite } = await api.getEvalSuite(slug);
@@ -2574,7 +2685,8 @@ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it c
2574
2685
  opts.frontendCommitSha,
2575
2686
  opts.backendCommitSha,
2576
2687
  opts.source,
2577
- opts.model
2688
+ opts.model,
2689
+ opts.rotateCases
2578
2690
  );
2579
2691
  console.log(`run_id: ${run_id}`);
2580
2692
  if (!opts.watch) return;
@@ -3916,17 +4028,21 @@ domainsCmd.command("list").description("List the org's custom domains with live
3916
4028
  }
3917
4029
  for (const d of domains) {
3918
4030
  const checked = d.last_checked_at ? `checked ${d.last_checked_at}` : "never checked";
4031
+ const managerDefault = d.manager_default ? " manager default" : "";
3919
4032
  const reason = d.error_reason ? ` ${d.error_reason}` : "";
3920
- console.log(`${d.domain} ${d.status} ${checked}${reason}`);
4033
+ console.log(`${d.domain} ${d.status} ${checked}${managerDefault}${reason}`);
3921
4034
  }
3922
4035
  } catch (e) {
3923
4036
  fail(e);
3924
4037
  }
3925
4038
  });
3926
- 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) => {
3927
4043
  try {
3928
- const d = await new ErdoClient().createCustomDomain(domain);
3929
- 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)" : ""}`);
3930
4046
  if (d.dns_records.length > 0) {
3931
4047
  process.stderr.write("Create these records at the domain's DNS provider:\n");
3932
4048
  for (const r of d.dns_records) {
@@ -3937,6 +4053,20 @@ domainsCmd.command("add <domain>").description("Register a custom domain (a dire
3937
4053
  fail(e);
3938
4054
  }
3939
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
+ });
3940
4070
  domainsCmd.command("remove <domain>").description("Remove a custom domain registration (stops it serving pages)").action(async (domain) => {
3941
4071
  try {
3942
4072
  const res = await new ErdoClient().deleteCustomDomain(domain);
@@ -4160,6 +4290,98 @@ sentEmailsCmd.command("list").description("List sent email, newest first, includ
4160
4290
  }
4161
4291
  }
4162
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
+ });
4163
4385
  sentEmailsCmd.command("get <emailID>").description("Read one sent email, including exact bodies and delivery evidence").action(async (emailID) => {
4164
4386
  try {
4165
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.66.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"