@erdoai/cli 0.67.0 → 0.71.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 +516 -21
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -297,14 +297,19 @@ var ErdoClient = class {
297
297
  createManagerKey() {
298
298
  return this.request("POST", "/v1/manager-key");
299
299
  }
300
- // Brings an EXISTING org under your manager org by redeeming a one-time consent
301
- // token a manager credential on its own can never adopt an arbitrary org, it
302
- // has to present a secret only the target org's owner (or, for an ownerless
303
- // managed org, its current manager) could have minted. The path is a sibling of
304
- // /v1/managed-organizations rather than a nested `adopt` because Encore rejects
305
- // a static segment alongside the parameterized /:orgSlug routes.
306
- adoptManagedOrganization(token) {
307
- return this.request("POST", "/v1/managed-organization-adoptions", { token });
300
+ // Brings an EXISTING org under your manager org. Two consents redeem here: a
301
+ // one-time token minted by the target org's owner (or, for an ownerless managed
302
+ // org, its current manager) — a manager credential on its own can never adopt an
303
+ // arbitrary org or, DIRECT adoption, the org's slug when the caller themselves
304
+ // is a seated owner of it (both halves of the consent in one person, so no token
305
+ // ferrying). The path is a sibling of /v1/managed-organizations rather than a
306
+ // nested `adopt` because Encore rejects a static segment alongside the
307
+ // parameterized /:orgSlug routes.
308
+ adoptManagedOrganization(input) {
309
+ return this.request("POST", "/v1/managed-organization-adoptions", {
310
+ token: input.token ?? "",
311
+ organization_slug: input.organizationSlug ?? ""
312
+ });
308
313
  }
309
314
  // Seats a person in a managed org. The role is capped at admin — ownership of a
310
315
  // client org stays with the client — and an omitted role means the backend's
@@ -331,8 +336,30 @@ var ErdoClient = class {
331
336
  listCustomDomains() {
332
337
  return this.request("GET", "/v1/custom-domains");
333
338
  }
334
- createCustomDomain(domain) {
335
- return this.request("POST", "/v1/custom-domains", { domain });
339
+ // `managerDefault` registers the domain and points the orgs this one manages at
340
+ // it in one step, for a manager setting up its very first branded hostname. The
341
+ // key is left out of the body when false so an ordinary registration sends what
342
+ // it always sent; changing the flag on a domain that is already live goes
343
+ // through setCustomDomainManagerDefault, which does not touch the certificate.
344
+ createCustomDomain(domain, managerDefault) {
345
+ return this.request(
346
+ "POST",
347
+ "/v1/custom-domains",
348
+ managerDefault ? { domain, manager_default: true } : { domain }
349
+ );
350
+ }
351
+ // Marks a domain the org already owns as the one its managed orgs serve their
352
+ // pages from, or clears it. At most one domain per org carries the flag, so
353
+ // promoting a second demotes the first in the same write — the managed orgs
354
+ // never see a moment with no default and fall back to platform URLs. Only a
355
+ // manager account with at least one client org can set it; clearing is always
356
+ // allowed, so an org that has since let its last client go can still tidy up.
357
+ setCustomDomainManagerDefault(domain, managerDefault) {
358
+ return this.request(
359
+ "PATCH",
360
+ `/v1/custom-domains/${encodeURIComponent(domain)}/manager-default`,
361
+ { manager_default: managerDefault }
362
+ );
336
363
  }
337
364
  deleteCustomDomain(domain) {
338
365
  return this.request(
@@ -412,6 +439,22 @@ var ErdoClient = class {
412
439
  getSentEmail(emailID) {
413
440
  return this.request("GET", `/v1/emails/${encodeURIComponent(emailID)}`);
414
441
  }
442
+ // Phone conversation records: inbound calls to a voice agent's own number and
443
+ // the outbound calls Erdo placed. Website chat widget conversations are a
444
+ // different resource — they live under /v1/voice/widget-conversations.
445
+ listVoiceCalls(params) {
446
+ const q = new URLSearchParams();
447
+ if (params?.agent) q.set("agent", params.agent);
448
+ if (params?.direction) q.set("direction", params.direction);
449
+ if (params?.limit !== void 0) q.set("limit", String(params.limit));
450
+ if (params?.offset !== void 0) q.set("offset", String(params.offset));
451
+ if (params?.cursor) q.set("cursor", params.cursor);
452
+ const qs = q.toString();
453
+ return this.request("GET", `/v1/voice/calls${qs ? `?${qs}` : ""}`);
454
+ }
455
+ getVoiceCall(callID) {
456
+ return this.request("GET", `/v1/voice/calls/${encodeURIComponent(callID)}`);
457
+ }
415
458
  // Run a read-only HogQL query against the org's page-analytics events. Rows are
416
459
  // positional per columns; enabled:false means page analytics is off for the org
417
460
  // (not zero traffic). A rejected query surfaces PostHog's message as the error.
@@ -847,6 +890,14 @@ var ErdoClient = class {
847
890
  listDatasetPurposes() {
848
891
  return this.request("GET", `/v1/datasets-purposes`);
849
892
  }
893
+ // Correct which dataset carries a purpose. Pass `purpose` to set one,
894
+ // `clear_purpose` to remove it; moving a purpose is clearing it from the old
895
+ // dataset and setting it on the right one. Setting a purpose the org already
896
+ // uses elsewhere succeeds and answers with a warning naming what it shadows,
897
+ // since one purpose is meant to map to one dataset.
898
+ setDatasetPurpose(slug, input) {
899
+ return this.request("POST", `/v1/datasets/${encodeURIComponent(slug)}/schema`, input);
900
+ }
850
901
  // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
851
902
  // `query` made every `erdo datasets query` fail with "question is required".
852
903
  //
@@ -888,6 +939,69 @@ var ErdoClient = class {
888
939
  listDatasetRevisions(slug) {
889
940
  return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
890
941
  }
942
+ // --- bounded outreach ---
943
+ putOutreachBatch(batch, body) {
944
+ return this.request(
945
+ "PUT",
946
+ `/v1/outreach-batches/${encodeURIComponent(batch)}`,
947
+ body
948
+ );
949
+ }
950
+ listOutreachBatches(opts = {}) {
951
+ const q = new URLSearchParams();
952
+ if (opts.limit !== void 0) q.set("limit", String(opts.limit));
953
+ if (opts.cursor) q.set("cursor", opts.cursor);
954
+ if (opts.initiative_ref) q.set("initiative_ref", opts.initiative_ref);
955
+ const qs = q.toString();
956
+ return this.request(
957
+ "GET",
958
+ `/v1/outreach-batches${qs ? `?${qs}` : ""}`
959
+ );
960
+ }
961
+ getOutreachBatch(batch) {
962
+ return this.request(
963
+ "GET",
964
+ `/v1/outreach-batches/${encodeURIComponent(batch)}`
965
+ );
966
+ }
967
+ listOutreachBatchRecipients(batch, opts = {}) {
968
+ const q = new URLSearchParams();
969
+ if (opts.limit !== void 0) q.set("limit", String(opts.limit));
970
+ if (opts.cursor) q.set("cursor", opts.cursor);
971
+ const qs = q.toString();
972
+ return this.request(
973
+ "GET",
974
+ `/v1/outreach-batches/${encodeURIComponent(batch)}/recipients${qs ? `?${qs}` : ""}`
975
+ );
976
+ }
977
+ actOnOutreachBatch(batch, body) {
978
+ return this.request(
979
+ "POST",
980
+ `/v1/outreach-batches/${encodeURIComponent(batch)}/actions`,
981
+ body
982
+ );
983
+ }
984
+ putOutreachConsent(body) {
985
+ return this.request("PUT", "/v1/outreach-consents", body);
986
+ }
987
+ listOutreachConsents(opts = {}) {
988
+ const q = new URLSearchParams();
989
+ if (opts.limit !== void 0) q.set("limit", String(opts.limit));
990
+ if (opts.cursor) q.set("cursor", opts.cursor);
991
+ const qs = q.toString();
992
+ return this.request(
993
+ "GET",
994
+ `/v1/outreach-consents${qs ? `?${qs}` : ""}`
995
+ );
996
+ }
997
+ getOutreachConsent(opts) {
998
+ const q = new URLSearchParams();
999
+ if (opts.grant_ref) q.set("grant_ref", opts.grant_ref);
1000
+ if (opts.recipient_ref) q.set("recipient_ref", opts.recipient_ref);
1001
+ if (opts.phone_raw) q.set("phone_raw", opts.phone_raw);
1002
+ if (opts.default_country) q.set("default_country", opts.default_country);
1003
+ return this.request("GET", `/v1/outreach-consents?${q.toString()}`);
1004
+ }
891
1005
  uploadDatasetFile(body) {
892
1006
  return this.request("POST", "/v1/datasets-upload", body);
893
1007
  }
@@ -1080,17 +1194,36 @@ var ErdoClient = class {
1080
1194
  `/v1/kv/${encodeURIComponent(slug)}/items/${encodeURIComponent(key)}`
1081
1195
  );
1082
1196
  }
1197
+ // --- page monitoring ---
1198
+ // These live here rather than being called through `request` from the command
1199
+ // layer: `request` is private, and reaching past it left the CLI's own
1200
+ // typecheck red.
1201
+ setPageMonitoring(id, body) {
1202
+ return this.request(
1203
+ "POST",
1204
+ `/v1/pages/monitoring/${encodeURIComponent(id)}`,
1205
+ body
1206
+ );
1207
+ }
1208
+ getPageMonitoring(id) {
1209
+ return this.request(
1210
+ "GET",
1211
+ `/v1/pages/monitoring/${encodeURIComponent(id)}`
1212
+ );
1213
+ }
1083
1214
  // --- agent runs ---
1084
1215
  listAgentRuns(opts) {
1085
1216
  const params = new URLSearchParams();
1086
1217
  if (opts.agent) params.set("agent_key", opts.agent);
1087
1218
  if (opts.thread) params.set("thread_id", opts.thread);
1219
+ if (opts.status) params.set("status", opts.status);
1088
1220
  if (opts.limit) params.set("limit", String(opts.limit));
1089
1221
  const qs = params.toString();
1090
1222
  return this.request("GET", `/v1/runs${qs ? `?${qs}` : ""}`);
1091
1223
  }
1092
- getAgentRun(id) {
1093
- return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
1224
+ getAgentRun(id, include = []) {
1225
+ const qs = include.length ? `?include=${encodeURIComponent(include.join(","))}` : "";
1226
+ return this.request("GET", `/v1/runs/${encodeURIComponent(id)}${qs}`);
1094
1227
  }
1095
1228
  // --- approvals ---
1096
1229
  listApprovals(opts) {
@@ -1534,6 +1667,25 @@ function timedOutMessage(threadID) {
1534
1667
  function print(value) {
1535
1668
  console.log(JSON.stringify(value, null, 2));
1536
1669
  }
1670
+ function readJSONObject(file, label) {
1671
+ let parsed;
1672
+ try {
1673
+ parsed = JSON.parse(readFileSync4(file, "utf8"));
1674
+ } catch (error) {
1675
+ throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`);
1676
+ }
1677
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
1678
+ throw new Error(`${label} must contain one JSON object`);
1679
+ }
1680
+ return parsed;
1681
+ }
1682
+ function requireOutreachProject() {
1683
+ if (!process.env.ERDO_PROJECT?.trim()) {
1684
+ throw new Error(
1685
+ "outreach commands require a project: pass --project <uuid> (and --org when the key can reach more than one organization)"
1686
+ );
1687
+ }
1688
+ }
1537
1689
  function printPageReview(review) {
1538
1690
  if (!review) return;
1539
1691
  if (!review.reviewed) {
@@ -1806,9 +1958,20 @@ id: ${o.id}`);
1806
1958
  fail(e);
1807
1959
  }
1808
1960
  });
1809
- managed.command("adopt <token>").description("Take over an existing org by redeeming a one-time consent token").action(async (token) => {
1961
+ managed.command("adopt [token]").description(
1962
+ "Take over an existing org: redeem a one-time consent token, or --from-org <slug> for an org you own yourself (no token needed)"
1963
+ ).option("--from-org <slug>", "slug of an org you are an owner of, adopted directly").action(async (token, opts) => {
1810
1964
  try {
1811
- const o = await new ErdoClient().adoptManagedOrganization(token);
1965
+ if (!token && !opts.fromOrg) {
1966
+ fail(new Error("provide a consent token, or --from-org <slug> for an org you own"));
1967
+ }
1968
+ if (token && opts.fromOrg) {
1969
+ fail(new Error("provide a token or --from-org, not both"));
1970
+ }
1971
+ const o = await new ErdoClient().adoptManagedOrganization({
1972
+ token,
1973
+ organizationSlug: opts.fromOrg
1974
+ });
1812
1975
  console.log(`Adopted managed org: ${o.name}`);
1813
1976
  console.log(`slug: ${o.slug}
1814
1977
  id: ${o.id}`);
@@ -2915,7 +3078,7 @@ agentCmd.command("messages <threadId>").description("Show a thread's messages (r
2915
3078
  }
2916
3079
  });
2917
3080
  var runsCmd = program.command("runs").description("Inspect agent runs");
2918
- runsCmd.command("list").description("List agent runs (filter by agent OR thread \u2014 mutually exclusive)").option("-a, --agent <key>", "filter by agent key").option("-t, --thread <id>", "filter to a thread").option("-l, --limit <n>", "max runs", (v) => parseInt(v, 10)).action(async (opts) => {
3081
+ runsCmd.command("list").description("List agent runs (filter by agent OR thread \u2014 mutually exclusive)").option("-a, --agent <key>", "filter by agent key").option("-t, --thread <id>", "filter to a thread").option("-s, --status <status>", "filter by status (running, completed, failed, cancelled, pending)").option("-l, --limit <n>", "max runs", (v) => parseInt(v, 10)).action(async (opts) => {
2919
3082
  try {
2920
3083
  const { runs } = await new ErdoClient().listAgentRuns(opts);
2921
3084
  for (const r of runs) console.log(`${r.id} ${r.status} ${r.agent_key}`);
@@ -2923,13 +3086,130 @@ runsCmd.command("list").description("List agent runs (filter by agent OR thread
2923
3086
  fail(e);
2924
3087
  }
2925
3088
  });
2926
- runsCmd.command("get <id>").description("Show an agent run (status, output, trace metadata)").action(async (id) => {
3089
+ runsCmd.command("get <id>").description("Show an agent run (status, output, trace metadata)").option("--resources", "also show the skills, knowledge and entities the run used").option("--steps", "also show the run's iterations, tool calls and sub-agent invocations").action(async (id, opts) => {
2927
3090
  try {
2928
- print(await new ErdoClient().getAgentRun(id));
3091
+ const include = [];
3092
+ if (opts.resources) include.push("resources");
3093
+ if (opts.steps) include.push("steps");
3094
+ const detail = await new ErdoClient().getAgentRun(id, include);
3095
+ if (include.length === 0) {
3096
+ print(detail);
3097
+ return;
3098
+ }
3099
+ printRunDetail(detail, opts.resources === true, opts.steps === true);
2929
3100
  } catch (e) {
2930
3101
  fail(e);
2931
3102
  }
2932
3103
  });
3104
+ var RUN_ARRIVAL_LABELS = {
3105
+ "knowledge.kind_pinned": "pinned for this kind",
3106
+ "knowledge.list": "pinned",
3107
+ "knowledge.search": "ambient search",
3108
+ "knowledge.tool_search": "fetched mid-run",
3109
+ "knowledge.tool_list": "listed mid-run",
3110
+ judge_lens: "review lens",
3111
+ persona_lens: "persona lens",
3112
+ read_knowledge_object: "read directly",
3113
+ definition: "definition",
3114
+ constraint_source: "constraint source"
3115
+ };
3116
+ function arrivalLabel(usageKind) {
3117
+ return RUN_ARRIVAL_LABELS[usageKind] || usageKind || "used";
3118
+ }
3119
+ function resourceKind(resource) {
3120
+ if (resource.resource_type === "query_constraint") return "query_constraint";
3121
+ return resource.type || resource.resource_type;
3122
+ }
3123
+ function printRunDetail(detail, wantResources, wantSteps) {
3124
+ const run = detail.run;
3125
+ console.log(`${run.id} ${run.status} ${run.agent_name || run.agent_key}`);
3126
+ if (wantResources) {
3127
+ const resources = detail.resources ?? [];
3128
+ console.log("");
3129
+ console.log("Resources used");
3130
+ if (resources.length === 0) {
3131
+ console.log(" (none recorded)");
3132
+ } else {
3133
+ const byKind = /* @__PURE__ */ new Map();
3134
+ for (const resource of resources) {
3135
+ const kind = resourceKind(resource);
3136
+ const key = resource.resource_id || resource.id;
3137
+ const group = byKind.get(kind) ?? /* @__PURE__ */ new Map();
3138
+ const entry = group.get(key) ?? {
3139
+ name: resource.display_name || resource.resource_id || resource.id,
3140
+ arrivals: []
3141
+ };
3142
+ const arrival = arrivalLabel(resource.usage_kind);
3143
+ if (!entry.arrivals.includes(arrival)) entry.arrivals.push(arrival);
3144
+ group.set(key, entry);
3145
+ byKind.set(kind, group);
3146
+ }
3147
+ const kinds = [...byKind.keys()].sort(
3148
+ (a, b) => a === "skill" ? -1 : b === "skill" ? 1 : a.localeCompare(b)
3149
+ );
3150
+ for (const kind of kinds) {
3151
+ console.log(` ${kind}`);
3152
+ for (const entry of byKind.get(kind).values()) {
3153
+ console.log(` ${entry.name} (${entry.arrivals.join(", ")})`);
3154
+ }
3155
+ }
3156
+ }
3157
+ }
3158
+ if (wantSteps) {
3159
+ const steps = detail.steps ?? [];
3160
+ console.log("");
3161
+ console.log("Steps");
3162
+ if (steps.length === 0) {
3163
+ console.log(" (none recorded)");
3164
+ } else {
3165
+ printStepTree(steps);
3166
+ }
3167
+ }
3168
+ }
3169
+ function printStepTree(steps) {
3170
+ const childrenOf = /* @__PURE__ */ new Map();
3171
+ const known = new Set(steps.map((s) => s.step_id));
3172
+ const roots = [];
3173
+ for (const step of steps) {
3174
+ if (step.parent_step_id && known.has(step.parent_step_id)) {
3175
+ const siblings = childrenOf.get(step.parent_step_id) ?? [];
3176
+ siblings.push(step);
3177
+ childrenOf.set(step.parent_step_id, siblings);
3178
+ } else {
3179
+ roots.push(step);
3180
+ }
3181
+ }
3182
+ const printed = /* @__PURE__ */ new Set();
3183
+ const walk = (step, depth) => {
3184
+ if (printed.has(step.step_id)) return;
3185
+ printed.add(step.step_id);
3186
+ console.log(`${" ".repeat(depth + 1)}${stepLine(step)}`);
3187
+ for (const child of childrenOf.get(step.step_id) ?? []) walk(child, depth + 1);
3188
+ };
3189
+ for (const root of roots) walk(root, 0);
3190
+ for (const step of steps) {
3191
+ if (!printed.has(step.step_id)) {
3192
+ printed.add(step.step_id);
3193
+ console.log(` ${stepLine(step)}`);
3194
+ }
3195
+ }
3196
+ }
3197
+ function stepLine(step) {
3198
+ const parts = [];
3199
+ if (step.kind === "agent") {
3200
+ parts.push(`agent ${step.agent_key || step.name}`);
3201
+ if (step.agent_run_id) parts.push(`run ${step.agent_run_id}`);
3202
+ } else if (step.kind === "tool_call") {
3203
+ parts.push(`tool ${step.name}`);
3204
+ } else {
3205
+ parts.push(step.iteration ? `iteration ${step.iteration}` : "iteration");
3206
+ if (step.name) parts.push(step.name);
3207
+ }
3208
+ if (step.duration_ms !== void 0) parts.push(`${step.duration_ms}ms`);
3209
+ if (step.status) parts.push(step.status);
3210
+ if (step.error) parts.push(`error: ${step.error}`);
3211
+ return parts.join(" ");
3212
+ }
2933
3213
  var decisionsCmd = program.command("decisions").description(
2934
3214
  "The decision record \u2014 what your organization committed to, whether the change actually happened, and what the evidence said afterwards"
2935
3215
  );
@@ -3839,6 +4119,32 @@ pagesCmd.command("restore <id>").description("Restore a previously deleted page
3839
4119
  fail(e);
3840
4120
  }
3841
4121
  });
4122
+ pagesCmd.command("monitor <id>").description(
4123
+ "Control health monitoring for a published page: --exclude stops all probing (dashboards, trackers, demos); --ack <sig> acknowledges a known failure class (mobile:overflow, desktop:resource) so it stops alerting and never auto-repairs while a NEW failure class still alerts"
4124
+ ).option("--exclude [reason]", "exclude this page from monitoring entirely (optional reason)").option("--include", "re-include the page in monitoring").option("--ack <sigs>", "comma-separated failure signatures to acknowledge (e.g. mobile:overflow,desktop:resource)").option("--unack <sigs>", "comma-separated failure signatures to un-acknowledge").option("--reset", "clear all acknowledged failure signatures").action(async (id, opts) => {
4125
+ try {
4126
+ const api = new ErdoClient();
4127
+ const body = {};
4128
+ if (opts.include) body.excluded = false;
4129
+ else if (typeof opts.exclude === "string") {
4130
+ body.excluded = true;
4131
+ body.excluded_reason = opts.exclude;
4132
+ } else if (opts.exclude === true) body.excluded = true;
4133
+ if (opts.ack) body.ack_add = opts.ack.split(",").map((s) => s.trim()).filter(Boolean);
4134
+ if (opts.unack) body.ack_remove = opts.unack.split(",").map((s) => s.trim()).filter(Boolean);
4135
+ if (opts.reset) body.ack_reset = true;
4136
+ print(await api.setPageMonitoring(id, body));
4137
+ } catch (e) {
4138
+ fail(e);
4139
+ }
4140
+ });
4141
+ pagesCmd.command("monitor-get <id>").description("Read a page's monitoring controls (excluded flag + acknowledged failure signatures)").action(async (id) => {
4142
+ try {
4143
+ print(await new ErdoClient().getPageMonitoring(id));
4144
+ } catch (e) {
4145
+ fail(e);
4146
+ }
4147
+ });
3842
4148
  pagesCmd.command("clone <id>").description(
3843
4149
  "Copy a page. The copy is byte-identical and starts private (publish state is never inherited); the source's lead-form pipelines are duplicated onto it and the ids in its content rewritten, so it captures its own leads"
3844
4150
  ).option("--title <title>", 'title for the copy (default: "Copy of <source title>")').option("--json", "print the full JSON, including the cloned pipelines and id rewrites").action(async (id, opts) => {
@@ -3924,17 +4230,21 @@ domainsCmd.command("list").description("List the org's custom domains with live
3924
4230
  }
3925
4231
  for (const d of domains) {
3926
4232
  const checked = d.last_checked_at ? `checked ${d.last_checked_at}` : "never checked";
4233
+ const managerDefault = d.manager_default ? " manager default" : "";
3927
4234
  const reason = d.error_reason ? ` ${d.error_reason}` : "";
3928
- console.log(`${d.domain} ${d.status} ${checked}${reason}`);
4235
+ console.log(`${d.domain} ${d.status} ${checked}${managerDefault}${reason}`);
3929
4236
  }
3930
4237
  } catch (e) {
3931
4238
  fail(e);
3932
4239
  }
3933
4240
  });
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) => {
4241
+ 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(
4242
+ "--manager-default",
4243
+ "also point the orgs you manage at this domain, for a manager registering its first branded hostname"
4244
+ ).action(async (domain, opts) => {
3935
4245
  try {
3936
- const d = await new ErdoClient().createCustomDomain(domain);
3937
- console.log(`${d.domain}: ${d.status}`);
4246
+ const d = await new ErdoClient().createCustomDomain(domain, opts.managerDefault);
4247
+ console.log(`${d.domain}: ${d.status}${d.manager_default ? " (manager default)" : ""}`);
3938
4248
  if (d.dns_records.length > 0) {
3939
4249
  process.stderr.write("Create these records at the domain's DNS provider:\n");
3940
4250
  for (const r of d.dns_records) {
@@ -3945,6 +4255,20 @@ domainsCmd.command("add <domain>").description("Register a custom domain (a dire
3945
4255
  fail(e);
3946
4256
  }
3947
4257
  });
4258
+ domainsCmd.command("manager-default <domain>").description(
4259
+ "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"
4260
+ ).option("--unset", "clear the flag instead, returning the managed orgs to the platform pages host").action(async (domain, opts) => {
4261
+ try {
4262
+ const d = await new ErdoClient().setCustomDomainManagerDefault(domain, !opts.unset);
4263
+ if (d.manager_default) {
4264
+ console.log(`${d.domain} is now the default domain for the orgs you manage (status: ${d.status})`);
4265
+ } else {
4266
+ console.log(`${d.domain} is no longer the default domain for the orgs you manage`);
4267
+ }
4268
+ } catch (e) {
4269
+ fail(e);
4270
+ }
4271
+ });
3948
4272
  domainsCmd.command("remove <domain>").description("Remove a custom domain registration (stops it serving pages)").action(async (domain) => {
3949
4273
  try {
3950
4274
  const res = await new ErdoClient().deleteCustomDomain(domain);
@@ -4168,6 +4492,98 @@ sentEmailsCmd.command("list").description("List sent email, newest first, includ
4168
4492
  }
4169
4493
  }
4170
4494
  );
4495
+ var outreachCmd = program.command("outreach").description("Consent-gated SMS outreach to an explicit reviewed selection of up to 700 people");
4496
+ var outreachBatchesCmd = outreachCmd.command("batches").description("Prepare and control outreach batches");
4497
+ 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) => {
4498
+ try {
4499
+ requireOutreachProject();
4500
+ print(await new ErdoClient().putOutreachBatch(batch, readJSONObject(opts.file, "--file")));
4501
+ } catch (e) {
4502
+ fail(e);
4503
+ }
4504
+ });
4505
+ 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) => {
4506
+ try {
4507
+ requireOutreachProject();
4508
+ print(await new ErdoClient().listOutreachBatches({
4509
+ limit: opts.limit,
4510
+ cursor: opts.cursor,
4511
+ initiative_ref: opts.initiative
4512
+ }));
4513
+ } catch (e) {
4514
+ fail(e);
4515
+ }
4516
+ });
4517
+ 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) => {
4518
+ try {
4519
+ requireOutreachProject();
4520
+ const client = new ErdoClient();
4521
+ print(
4522
+ opts.recipients ? await client.listOutreachBatchRecipients(batch, opts) : await client.getOutreachBatch(batch)
4523
+ );
4524
+ } catch (e) {
4525
+ fail(e);
4526
+ }
4527
+ });
4528
+ 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(
4529
+ async (batch, action, opts) => {
4530
+ try {
4531
+ requireOutreachProject();
4532
+ print(
4533
+ await new ErdoClient().actOnOutreachBatch(batch, {
4534
+ action,
4535
+ action_ref: opts.actionRef,
4536
+ expected_preview_revision: opts.previewRevision,
4537
+ expected_eligible_count: opts.eligibleCount,
4538
+ expected_message_hash: opts.messageHash,
4539
+ expected_source_snapshot_ref: opts.sourceSnapshot,
4540
+ expected_batch_state: opts.batchState,
4541
+ expected_batch_updated_at: opts.batchUpdatedAt
4542
+ })
4543
+ );
4544
+ } catch (e) {
4545
+ fail(e);
4546
+ }
4547
+ }
4548
+ );
4549
+ var outreachConsentsCmd = outreachCmd.command("consents").description("Declare and read auditable SMS consent evidence");
4550
+ 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) => {
4551
+ try {
4552
+ requireOutreachProject();
4553
+ print(await new ErdoClient().putOutreachConsent(readJSONObject(opts.file, "--file")));
4554
+ } catch (e) {
4555
+ fail(e);
4556
+ }
4557
+ });
4558
+ 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) => {
4559
+ try {
4560
+ requireOutreachProject();
4561
+ print(await new ErdoClient().listOutreachConsents(opts));
4562
+ } catch (e) {
4563
+ fail(e);
4564
+ }
4565
+ });
4566
+ 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) => {
4567
+ try {
4568
+ requireOutreachProject();
4569
+ const byGrant = Boolean(grantRef);
4570
+ const byRecipient = Boolean(opts.recipientRef || opts.phone || opts.country);
4571
+ if (byGrant === byRecipient) {
4572
+ throw new Error("consents get requires either <grant-ref> or all of --recipient-ref, --phone, and --country");
4573
+ }
4574
+ if (!byGrant && !(opts.recipientRef && opts.phone && opts.country)) {
4575
+ throw new Error("consents get by recipient requires --recipient-ref, --phone, and --country together");
4576
+ }
4577
+ print(await new ErdoClient().getOutreachConsent({
4578
+ grant_ref: grantRef,
4579
+ recipient_ref: opts.recipientRef,
4580
+ phone_raw: opts.phone,
4581
+ default_country: opts.country
4582
+ }));
4583
+ } catch (e) {
4584
+ fail(e);
4585
+ }
4586
+ });
4171
4587
  sentEmailsCmd.command("get <emailID>").description("Read one sent email, including exact bodies and delivery evidence").action(async (emailID) => {
4172
4588
  try {
4173
4589
  print(await new ErdoClient().getSentEmail(emailID));
@@ -4175,6 +4591,59 @@ sentEmailsCmd.command("get <emailID>").description("Read one sent email, includi
4175
4591
  fail(e);
4176
4592
  }
4177
4593
  });
4594
+ var voiceCmd = program.command("voice").description("Read voice agent phone conversations");
4595
+ var voiceCallsCmd = voiceCmd.command("calls").description("Inbound and outbound phone call records");
4596
+ 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("--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(
4597
+ async (opts) => {
4598
+ try {
4599
+ const res = await new ErdoClient().listVoiceCalls({
4600
+ agent: opts.agent,
4601
+ direction: opts.direction,
4602
+ limit: opts.limit ? Number(opts.limit) : void 0,
4603
+ offset: opts.offset ? Number(opts.offset) : void 0,
4604
+ cursor: opts.cursor
4605
+ });
4606
+ if (opts.json) {
4607
+ print(res);
4608
+ return;
4609
+ }
4610
+ const calls = res.conversations ?? [];
4611
+ if (calls.length === 0) {
4612
+ console.log("No calls match those filters.");
4613
+ return;
4614
+ }
4615
+ printAlignedTable(
4616
+ ["call id", "direction", "from", "to", "status", "secs", "transcript", "started", "summary"],
4617
+ calls.map((call) => [
4618
+ call.call_id,
4619
+ call.direction,
4620
+ call.from_number ?? "",
4621
+ call.to_number,
4622
+ call.status,
4623
+ call.duration_seconds,
4624
+ call.has_transcript ? "yes" : "no",
4625
+ call.created_at,
4626
+ call.transcript_summary
4627
+ ])
4628
+ );
4629
+ process.stderr.write(`showing ${calls.length} call(s)
4630
+ `);
4631
+ if (res.next_cursor) {
4632
+ process.stderr.write(`more available \u2014 re-run with --cursor ${res.next_cursor}
4633
+ `);
4634
+ }
4635
+ } catch (e) {
4636
+ fail(e);
4637
+ }
4638
+ }
4639
+ );
4640
+ voiceCallsCmd.command("get <callID>").description("Read one phone call in full: transcript, summary, and per-turn LLM metrics").action(async (callID) => {
4641
+ try {
4642
+ print(await new ErdoClient().getVoiceCall(callID));
4643
+ } catch (e) {
4644
+ fail(e);
4645
+ }
4646
+ });
4178
4647
  var datasetsCmd = program.command("datasets").description("Datasets");
4179
4648
  datasetsCmd.command("list").description("List datasets").option(
4180
4649
  "--class <class>",
@@ -4219,6 +4688,32 @@ datasetsCmd.command("purposes").description(
4219
4688
  fail(e);
4220
4689
  }
4221
4690
  });
4691
+ datasetsCmd.command("set-purpose <slug> [purpose]").description(
4692
+ "Set (or with --clear, remove) the dataset's purpose \u2014 the org-vocabulary role `datasets purposes` reports. Use it to CORRECT a purpose that names the wrong dataset: one purpose maps to one dataset per org, so a purpose left on the wrong one points everything that resolves through the vocabulary at the wrong rows. Moving a purpose is two calls \u2014 clear it from the old dataset, set it on the right one. Setting a purpose the org already uses elsewhere succeeds and prints a warning naming what it now shadows."
4693
+ ).option("--clear", "remove the dataset's purpose instead of setting one").option(
4694
+ "--description <text>",
4695
+ "description for a purpose new to this org; ignored when the org already has an entry for it"
4696
+ ).action(async (slug, purpose, opts) => {
4697
+ try {
4698
+ if (opts.clear && purpose) {
4699
+ fail(new Error("pass a purpose or --clear, not both"));
4700
+ return;
4701
+ }
4702
+ if (!opts.clear && !purpose) {
4703
+ fail(new Error("pass a purpose to set, or --clear to remove one"));
4704
+ return;
4705
+ }
4706
+ const res = await new ErdoClient().setDatasetPurpose(slug, {
4707
+ ...purpose ? { purpose } : {},
4708
+ ...opts.clear ? { clear_purpose: true } : {},
4709
+ ...opts.description ? { purpose_description: opts.description } : {}
4710
+ });
4711
+ for (const w of res.warnings ?? []) console.error(`warning: ${w}`);
4712
+ console.log(res.purpose ? `${slug} ${res.purpose}` : `${slug} (no purpose)`);
4713
+ } catch (e) {
4714
+ fail(e);
4715
+ }
4716
+ });
4222
4717
  datasetsCmd.command("query <slug> <question>").description(
4223
4718
  "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`."
4224
4719
  ).action(async (slug, question) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.67.0",
4
- "description": "Erdo CLI drive datasets, pages, and evals from the terminal or CI",
3
+ "version": "0.71.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"
@@ -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"
@@ -53,4 +53,4 @@
53
53
  "overrides": {
54
54
  "esbuild": "^0.28.1"
55
55
  }
56
- }
56
+ }