@erdoai/cli 0.68.0 → 0.72.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 +411 -17
  2. package/package.json +3 -3
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
@@ -327,6 +332,33 @@ var ErdoClient = class {
327
332
  { successor_manager_slug: successorManagerSlug }
328
333
  );
329
334
  }
335
+ // Reads the Google Ads container a managed org already has under this manager
336
+ // and creates nothing, so it is safe to run against an org mid-setup: a 404
337
+ // means the org has no container here, not that anything is broken.
338
+ getManagedOrganizationAdsContainer(slug) {
339
+ return this.request(
340
+ "GET",
341
+ `/v1/managed-organizations/${encodeURIComponent(slug)}/ads-container`
342
+ );
343
+ }
344
+ // Creates the container — a sub-account under the manager's MCC plus the
345
+ // delegated connection inside the managed org. `replaceCurrentAccount` is the
346
+ // one input with consequences: provisioning refuses outright when the org is
347
+ // already running campaigns in another account, and passing this accepts that
348
+ // those campaigns are left behind, so the key is sent only when it is asked
349
+ // for rather than as a `false` the server has to read.
350
+ provisionManagedOrganizationAdsContainer(slug, input) {
351
+ const body = {};
352
+ if (input.descriptiveName) body.descriptive_name = input.descriptiveName;
353
+ if (input.currencyCode) body.currency_code = input.currencyCode;
354
+ if (input.timeZone) body.time_zone = input.timeZone;
355
+ if (input.replaceCurrentAccount) body.replace_current_account = true;
356
+ return this.request(
357
+ "POST",
358
+ `/v1/managed-organizations/${encodeURIComponent(slug)}/ads-container`,
359
+ body
360
+ );
361
+ }
330
362
  // --- Custom page domains: the branded hostnames the org's pages serve from ---
331
363
  listCustomDomains() {
332
364
  return this.request("GET", "/v1/custom-domains");
@@ -434,6 +466,22 @@ var ErdoClient = class {
434
466
  getSentEmail(emailID) {
435
467
  return this.request("GET", `/v1/emails/${encodeURIComponent(emailID)}`);
436
468
  }
469
+ // Phone conversation records: inbound calls to a voice agent's own number and
470
+ // the outbound calls Erdo placed. Website chat widget conversations are a
471
+ // different resource — they live under /v1/voice/widget-conversations.
472
+ listVoiceCalls(params) {
473
+ const q = new URLSearchParams();
474
+ if (params?.agent) q.set("agent", params.agent);
475
+ if (params?.direction) q.set("direction", params.direction);
476
+ if (params?.limit !== void 0) q.set("limit", String(params.limit));
477
+ if (params?.offset !== void 0) q.set("offset", String(params.offset));
478
+ if (params?.cursor) q.set("cursor", params.cursor);
479
+ const qs = q.toString();
480
+ return this.request("GET", `/v1/voice/calls${qs ? `?${qs}` : ""}`);
481
+ }
482
+ getVoiceCall(callID) {
483
+ return this.request("GET", `/v1/voice/calls/${encodeURIComponent(callID)}`);
484
+ }
437
485
  // Run a read-only HogQL query against the org's page-analytics events. Rows are
438
486
  // positional per columns; enabled:false means page analytics is off for the org
439
487
  // (not zero traffic). A rejected query surfaces PostHog's message as the error.
@@ -869,6 +917,14 @@ var ErdoClient = class {
869
917
  listDatasetPurposes() {
870
918
  return this.request("GET", `/v1/datasets-purposes`);
871
919
  }
920
+ // Correct which dataset carries a purpose. Pass `purpose` to set one,
921
+ // `clear_purpose` to remove it; moving a purpose is clearing it from the old
922
+ // dataset and setting it on the right one. Setting a purpose the org already
923
+ // uses elsewhere succeeds and answers with a warning naming what it shadows,
924
+ // since one purpose is meant to map to one dataset.
925
+ setDatasetPurpose(slug, input) {
926
+ return this.request("POST", `/v1/datasets/${encodeURIComponent(slug)}/schema`, input);
927
+ }
872
928
  // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
873
929
  // `query` made every `erdo datasets query` fail with "question is required".
874
930
  //
@@ -1165,17 +1221,36 @@ var ErdoClient = class {
1165
1221
  `/v1/kv/${encodeURIComponent(slug)}/items/${encodeURIComponent(key)}`
1166
1222
  );
1167
1223
  }
1224
+ // --- page monitoring ---
1225
+ // These live here rather than being called through `request` from the command
1226
+ // layer: `request` is private, and reaching past it left the CLI's own
1227
+ // typecheck red.
1228
+ setPageMonitoring(id, body) {
1229
+ return this.request(
1230
+ "POST",
1231
+ `/v1/pages/monitoring/${encodeURIComponent(id)}`,
1232
+ body
1233
+ );
1234
+ }
1235
+ getPageMonitoring(id) {
1236
+ return this.request(
1237
+ "GET",
1238
+ `/v1/pages/monitoring/${encodeURIComponent(id)}`
1239
+ );
1240
+ }
1168
1241
  // --- agent runs ---
1169
1242
  listAgentRuns(opts) {
1170
1243
  const params = new URLSearchParams();
1171
1244
  if (opts.agent) params.set("agent_key", opts.agent);
1172
1245
  if (opts.thread) params.set("thread_id", opts.thread);
1246
+ if (opts.status) params.set("status", opts.status);
1173
1247
  if (opts.limit) params.set("limit", String(opts.limit));
1174
1248
  const qs = params.toString();
1175
1249
  return this.request("GET", `/v1/runs${qs ? `?${qs}` : ""}`);
1176
1250
  }
1177
- getAgentRun(id) {
1178
- return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
1251
+ getAgentRun(id, include = []) {
1252
+ const qs = include.length ? `?include=${encodeURIComponent(include.join(","))}` : "";
1253
+ return this.request("GET", `/v1/runs/${encodeURIComponent(id)}${qs}`);
1179
1254
  }
1180
1255
  // --- approvals ---
1181
1256
  listApprovals(opts) {
@@ -1910,9 +1985,20 @@ id: ${o.id}`);
1910
1985
  fail(e);
1911
1986
  }
1912
1987
  });
1913
- managed.command("adopt <token>").description("Take over an existing org by redeeming a one-time consent token").action(async (token) => {
1988
+ managed.command("adopt [token]").description(
1989
+ "Take over an existing org: redeem a one-time consent token, or --from-org <slug> for an org you own yourself (no token needed)"
1990
+ ).option("--from-org <slug>", "slug of an org you are an owner of, adopted directly").action(async (token, opts) => {
1914
1991
  try {
1915
- const o = await new ErdoClient().adoptManagedOrganization(token);
1992
+ if (!token && !opts.fromOrg) {
1993
+ fail(new Error("provide a consent token, or --from-org <slug> for an org you own"));
1994
+ }
1995
+ if (token && opts.fromOrg) {
1996
+ fail(new Error("provide a token or --from-org, not both"));
1997
+ }
1998
+ const o = await new ErdoClient().adoptManagedOrganization({
1999
+ token,
2000
+ organizationSlug: opts.fromOrg
2001
+ });
1916
2002
  console.log(`Adopted managed org: ${o.name}`);
1917
2003
  console.log(`slug: ${o.slug}
1918
2004
  id: ${o.id}`);
@@ -1942,6 +2028,76 @@ managed.command("add-member <orgSlug> <email>").description("Seat a person in a
1942
2028
  fail(e);
1943
2029
  }
1944
2030
  });
2031
+ function printAdsContainer(c) {
2032
+ console.log(`org: ${c.org_slug}`);
2033
+ console.log(`customer id: ${c.customer_id}`);
2034
+ console.log(`manager (login customer id): ${c.login_customer_id}`);
2035
+ console.log(`integration: ${c.integration_id}`);
2036
+ if (c.descriptive_name) {
2037
+ console.log(`name: ${c.descriptive_name}`);
2038
+ }
2039
+ console.log(`billing: ${c.billing_status}`);
2040
+ if (c.billing_status === "unknown") {
2041
+ process.stderr.write(
2042
+ `Billing could not be checked for ${c.customer_id} \u2014 open the account in the Google Ads UI and confirm; this is not evidence that billing is missing.
2043
+ `
2044
+ );
2045
+ } else if (!c.billing_configured) {
2046
+ process.stderr.write(
2047
+ `Billing is ${c.billing_status}: ${c.customer_id} cannot serve until a payments profile is linked to it in the Google Ads UI.
2048
+ `
2049
+ );
2050
+ }
2051
+ }
2052
+ managed.command("ads-container <orgSlug>").description(
2053
+ "Read the Google Ads account a managed org runs its campaigns in, or --provision one under your manager account"
2054
+ ).option("--provision", "create the account and the org's delegated google_ads connection instead of reading them").option(
2055
+ "--replace-current-account",
2056
+ "with --provision: go ahead even though the org already runs campaigns in another account. Google Ads cannot move a campaign, so those campaigns stay behind and have to be rebuilt"
2057
+ ).option("--name <descriptiveName>", "display name for the new account (default: the org's name and slug)").option("--currency <code>", "ISO currency code for the new account, e.g. USD").option("--time-zone <tz>", "IANA time zone for the new account, e.g. America/New_York").action(
2058
+ async (orgSlug, opts) => {
2059
+ try {
2060
+ if (opts.replaceCurrentAccount && !opts.provision) {
2061
+ fail(new Error("--replace-current-account only applies with --provision"));
2062
+ }
2063
+ const api = new ErdoClient();
2064
+ if (!opts.provision) {
2065
+ try {
2066
+ printAdsContainer(await api.getManagedOrganizationAdsContainer(orgSlug));
2067
+ } catch (e) {
2068
+ if (e instanceof ErdoApiError && e.status === 404) {
2069
+ fail(
2070
+ new Error(
2071
+ `${orgSlug} has no Google Ads container under this manager. Create one with: erdo org managed ads-container ${orgSlug} --provision`
2072
+ )
2073
+ );
2074
+ }
2075
+ throw e;
2076
+ }
2077
+ return;
2078
+ }
2079
+ const c = await api.provisionManagedOrganizationAdsContainer(orgSlug, {
2080
+ descriptiveName: opts.name,
2081
+ currencyCode: opts.currency,
2082
+ timeZone: opts.timeZone,
2083
+ replaceCurrentAccount: opts.replaceCurrentAccount
2084
+ });
2085
+ console.log(
2086
+ c.already_provisioned ? `Google Ads container already existed for ${c.org_slug}` : `Created Google Ads container for ${c.org_slug}`
2087
+ );
2088
+ printAdsContainer(c);
2089
+ const leftBehind = c.previous_campaign_count ?? 0;
2090
+ if (c.previous_customer_id && leftBehind > 0) {
2091
+ process.stderr.write(
2092
+ `${leftBehind} campaign${leftBehind === 1 ? "" : "s"} stayed in ${c.previous_customer_id}, the account this replaced. Google Ads cannot move a campaign between accounts and nothing in ${c.org_slug} resolves that account any more, so they have to be rebuilt in ${c.customer_id}.
2093
+ `
2094
+ );
2095
+ }
2096
+ } catch (e) {
2097
+ fail(e);
2098
+ }
2099
+ }
2100
+ );
1945
2101
  managed.command("handoff-token <orgSlug>").description("Mint a one-time token offering an ownerless managed org to another manager; shown ONCE").requiredOption("--successor <managerOrgSlug>", "the manager org allowed to redeem the token").action(async (orgSlug, opts) => {
1946
2102
  try {
1947
2103
  const res = await new ErdoClient().createManagedOrganizationHandoffToken(orgSlug, opts.successor);
@@ -3019,7 +3175,7 @@ agentCmd.command("messages <threadId>").description("Show a thread's messages (r
3019
3175
  }
3020
3176
  });
3021
3177
  var runsCmd = program.command("runs").description("Inspect agent runs");
3022
- 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) => {
3178
+ 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) => {
3023
3179
  try {
3024
3180
  const { runs } = await new ErdoClient().listAgentRuns(opts);
3025
3181
  for (const r of runs) console.log(`${r.id} ${r.status} ${r.agent_key}`);
@@ -3027,13 +3183,130 @@ runsCmd.command("list").description("List agent runs (filter by agent OR thread
3027
3183
  fail(e);
3028
3184
  }
3029
3185
  });
3030
- runsCmd.command("get <id>").description("Show an agent run (status, output, trace metadata)").action(async (id) => {
3186
+ 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) => {
3031
3187
  try {
3032
- print(await new ErdoClient().getAgentRun(id));
3188
+ const include = [];
3189
+ if (opts.resources) include.push("resources");
3190
+ if (opts.steps) include.push("steps");
3191
+ const detail = await new ErdoClient().getAgentRun(id, include);
3192
+ if (include.length === 0) {
3193
+ print(detail);
3194
+ return;
3195
+ }
3196
+ printRunDetail(detail, opts.resources === true, opts.steps === true);
3033
3197
  } catch (e) {
3034
3198
  fail(e);
3035
3199
  }
3036
3200
  });
3201
+ var RUN_ARRIVAL_LABELS = {
3202
+ "knowledge.kind_pinned": "pinned for this kind",
3203
+ "knowledge.list": "pinned",
3204
+ "knowledge.search": "ambient search",
3205
+ "knowledge.tool_search": "fetched mid-run",
3206
+ "knowledge.tool_list": "listed mid-run",
3207
+ judge_lens: "review lens",
3208
+ persona_lens: "persona lens",
3209
+ read_knowledge_object: "read directly",
3210
+ definition: "definition",
3211
+ constraint_source: "constraint source"
3212
+ };
3213
+ function arrivalLabel(usageKind) {
3214
+ return RUN_ARRIVAL_LABELS[usageKind] || usageKind || "used";
3215
+ }
3216
+ function resourceKind(resource) {
3217
+ if (resource.resource_type === "query_constraint") return "query_constraint";
3218
+ return resource.type || resource.resource_type;
3219
+ }
3220
+ function printRunDetail(detail, wantResources, wantSteps) {
3221
+ const run = detail.run;
3222
+ console.log(`${run.id} ${run.status} ${run.agent_name || run.agent_key}`);
3223
+ if (wantResources) {
3224
+ const resources = detail.resources ?? [];
3225
+ console.log("");
3226
+ console.log("Resources used");
3227
+ if (resources.length === 0) {
3228
+ console.log(" (none recorded)");
3229
+ } else {
3230
+ const byKind = /* @__PURE__ */ new Map();
3231
+ for (const resource of resources) {
3232
+ const kind = resourceKind(resource);
3233
+ const key = resource.resource_id || resource.id;
3234
+ const group = byKind.get(kind) ?? /* @__PURE__ */ new Map();
3235
+ const entry = group.get(key) ?? {
3236
+ name: resource.display_name || resource.resource_id || resource.id,
3237
+ arrivals: []
3238
+ };
3239
+ const arrival = arrivalLabel(resource.usage_kind);
3240
+ if (!entry.arrivals.includes(arrival)) entry.arrivals.push(arrival);
3241
+ group.set(key, entry);
3242
+ byKind.set(kind, group);
3243
+ }
3244
+ const kinds = [...byKind.keys()].sort(
3245
+ (a, b) => a === "skill" ? -1 : b === "skill" ? 1 : a.localeCompare(b)
3246
+ );
3247
+ for (const kind of kinds) {
3248
+ console.log(` ${kind}`);
3249
+ for (const entry of byKind.get(kind).values()) {
3250
+ console.log(` ${entry.name} (${entry.arrivals.join(", ")})`);
3251
+ }
3252
+ }
3253
+ }
3254
+ }
3255
+ if (wantSteps) {
3256
+ const steps = detail.steps ?? [];
3257
+ console.log("");
3258
+ console.log("Steps");
3259
+ if (steps.length === 0) {
3260
+ console.log(" (none recorded)");
3261
+ } else {
3262
+ printStepTree(steps);
3263
+ }
3264
+ }
3265
+ }
3266
+ function printStepTree(steps) {
3267
+ const childrenOf = /* @__PURE__ */ new Map();
3268
+ const known = new Set(steps.map((s) => s.step_id));
3269
+ const roots = [];
3270
+ for (const step of steps) {
3271
+ if (step.parent_step_id && known.has(step.parent_step_id)) {
3272
+ const siblings = childrenOf.get(step.parent_step_id) ?? [];
3273
+ siblings.push(step);
3274
+ childrenOf.set(step.parent_step_id, siblings);
3275
+ } else {
3276
+ roots.push(step);
3277
+ }
3278
+ }
3279
+ const printed = /* @__PURE__ */ new Set();
3280
+ const walk = (step, depth) => {
3281
+ if (printed.has(step.step_id)) return;
3282
+ printed.add(step.step_id);
3283
+ console.log(`${" ".repeat(depth + 1)}${stepLine(step)}`);
3284
+ for (const child of childrenOf.get(step.step_id) ?? []) walk(child, depth + 1);
3285
+ };
3286
+ for (const root of roots) walk(root, 0);
3287
+ for (const step of steps) {
3288
+ if (!printed.has(step.step_id)) {
3289
+ printed.add(step.step_id);
3290
+ console.log(` ${stepLine(step)}`);
3291
+ }
3292
+ }
3293
+ }
3294
+ function stepLine(step) {
3295
+ const parts = [];
3296
+ if (step.kind === "agent") {
3297
+ parts.push(`agent ${step.agent_key || step.name}`);
3298
+ if (step.agent_run_id) parts.push(`run ${step.agent_run_id}`);
3299
+ } else if (step.kind === "tool_call") {
3300
+ parts.push(`tool ${step.name}`);
3301
+ } else {
3302
+ parts.push(step.iteration ? `iteration ${step.iteration}` : "iteration");
3303
+ if (step.name) parts.push(step.name);
3304
+ }
3305
+ if (step.duration_ms !== void 0) parts.push(`${step.duration_ms}ms`);
3306
+ if (step.status) parts.push(step.status);
3307
+ if (step.error) parts.push(`error: ${step.error}`);
3308
+ return parts.join(" ");
3309
+ }
3037
3310
  var decisionsCmd = program.command("decisions").description(
3038
3311
  "The decision record \u2014 what your organization committed to, whether the change actually happened, and what the evidence said afterwards"
3039
3312
  );
@@ -3833,7 +4106,13 @@ function grantList(v) {
3833
4106
  pagesCmd.command("deploy").description("Deploy a page (HTML/React); --html/--js/--css accept @file").requiredOption("--title <title>", "page title").requiredOption("--html <htmlOr@file>", "HTML (or @path to a file)").option("--js <jsOr@file>", "JS/JSX (or @path)").option("--css <cssOr@file>", "CSS (or @path)").option("--runtime <runtime>", "react-tailwind (default) or none").option(
3834
4107
  "--meta-title <title>",
3835
4108
  "public SEO title (browser tab + shared-link headline) \u2014 distinct from --title, the internal name"
3836
- ).option("--meta-description <description>", "public SEO description (shared-link preview / search result blurb)").option("--datasets <csv>", "dataset slugs the page reads").option("--writable-datasets <csv>", "dataset slugs the page may write via erdo.insertRows").option("--kv <csv>", "named KV-store slugs the page reads").option("--writable-kv <csv>", "named KV-store slugs the page may write").option("--public", "make the page publicly viewable").action(
4109
+ ).option("--meta-description <description>", "public SEO description (shared-link preview / search result blurb)").option(
4110
+ "--meta-icon-asset <attachmentId>",
4111
+ "public favicon: a Brand Style attachment id (apple-icon-180x180 / favicon-32x32) for the browser-tab icon"
4112
+ ).option(
4113
+ "--meta-image-asset <attachmentId>",
4114
+ "public share image: a Brand Style attachment id (og-image / hero photo) for the link-preview card"
4115
+ ).option("--datasets <csv>", "dataset slugs the page reads").option("--writable-datasets <csv>", "dataset slugs the page may write via erdo.insertRows").option("--kv <csv>", "named KV-store slugs the page reads").option("--writable-kv <csv>", "named KV-store slugs the page may write").option("--public", "make the page publicly viewable").action(
3837
4116
  async (opts) => {
3838
4117
  try {
3839
4118
  const csv = (v) => v ? v.split(",").map((s) => s.trim()) : void 0;
@@ -3845,6 +4124,8 @@ pagesCmd.command("deploy").description("Deploy a page (HTML/React); --html/--js/
3845
4124
  runtime: opts.runtime,
3846
4125
  meta_title: opts.metaTitle,
3847
4126
  meta_description: opts.metaDescription,
4127
+ meta_icon_asset_id: opts.metaIconAsset,
4128
+ meta_image_asset_id: opts.metaImageAsset,
3848
4129
  dataset_slugs: csv(opts.datasets),
3849
4130
  writable_dataset_slugs: csv(opts.writableDatasets),
3850
4131
  kv_slugs: csv(opts.kv),
@@ -3863,7 +4144,13 @@ pagesCmd.command("update <id>").description(
3863
4144
  ).option("--title <title>", "new title").option("--html <htmlOr@file>", "HTML (or @path to a file)").option("--js <jsOr@file>", "JS/JSX (or @path)").option("--css <cssOr@file>", "CSS (or @path)").option(
3864
4145
  "--meta-title <title>",
3865
4146
  'new public SEO title (omit to keep current; pass "" to clear back to the default)'
3866
- ).option("--meta-description <description>", 'new public SEO description (omit to keep current; pass "" to clear)').option("--datasets <csv>", 'replacement read-dataset slugs ("[]" clears)').option("--writable-datasets <csv>", 'replacement writable-dataset slugs ("[]" clears)').option("--kv <csv>", 'replacement read KV-store slugs ("[]" clears)').option("--writable-kv <csv>", 'replacement writable KV-store slugs ("[]" clears)').option("--public", "make the page publicly viewable").option("--private", "make the page private").option(
4147
+ ).option("--meta-description <description>", 'new public SEO description (omit to keep current; pass "" to clear)').option(
4148
+ "--meta-icon-asset <attachmentId>",
4149
+ 'new public favicon: a Brand Style attachment id (apple-icon-180x180 / favicon-32x32) (omit to keep current; pass "" to clear back to the default)'
4150
+ ).option(
4151
+ "--meta-image-asset <attachmentId>",
4152
+ 'new public share image: a Brand Style attachment id (og-image / hero photo) (omit to keep current; pass "" to clear)'
4153
+ ).option("--datasets <csv>", 'replacement read-dataset slugs ("[]" clears)').option("--writable-datasets <csv>", 'replacement writable-dataset slugs ("[]" clears)').option("--kv <csv>", 'replacement read KV-store slugs ("[]" clears)').option("--writable-kv <csv>", 'replacement writable KV-store slugs ("[]" clears)').option("--public", "make the page publicly viewable").option("--private", "make the page private").option(
3867
4154
  "--request-review",
3868
4155
  "ask the judge lenses to review the page after this update and report findings -- for material edits to a LIVE page (layout, forms/CTA, legal/compliance copy, restructured sections), not cosmetic tweaks. First builds and full rewrites are already reviewed automatically"
3869
4156
  ).action(
@@ -3876,6 +4163,8 @@ pagesCmd.command("update <id>").description(
3876
4163
  css: readMaybeFile(opts.css),
3877
4164
  meta_title: opts.metaTitle,
3878
4165
  meta_description: opts.metaDescription,
4166
+ meta_icon_asset_id: opts.metaIconAsset,
4167
+ meta_image_asset_id: opts.metaImageAsset,
3879
4168
  dataset_slugs: grantList(opts.datasets),
3880
4169
  writable_dataset_slugs: grantList(opts.writableDatasets),
3881
4170
  kv_slugs: grantList(opts.kv),
@@ -3943,6 +4232,32 @@ pagesCmd.command("restore <id>").description("Restore a previously deleted page
3943
4232
  fail(e);
3944
4233
  }
3945
4234
  });
4235
+ pagesCmd.command("monitor <id>").description(
4236
+ "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"
4237
+ ).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) => {
4238
+ try {
4239
+ const api = new ErdoClient();
4240
+ const body = {};
4241
+ if (opts.include) body.excluded = false;
4242
+ else if (typeof opts.exclude === "string") {
4243
+ body.excluded = true;
4244
+ body.excluded_reason = opts.exclude;
4245
+ } else if (opts.exclude === true) body.excluded = true;
4246
+ if (opts.ack) body.ack_add = opts.ack.split(",").map((s) => s.trim()).filter(Boolean);
4247
+ if (opts.unack) body.ack_remove = opts.unack.split(",").map((s) => s.trim()).filter(Boolean);
4248
+ if (opts.reset) body.ack_reset = true;
4249
+ print(await api.setPageMonitoring(id, body));
4250
+ } catch (e) {
4251
+ fail(e);
4252
+ }
4253
+ });
4254
+ pagesCmd.command("monitor-get <id>").description("Read a page's monitoring controls (excluded flag + acknowledged failure signatures)").action(async (id) => {
4255
+ try {
4256
+ print(await new ErdoClient().getPageMonitoring(id));
4257
+ } catch (e) {
4258
+ fail(e);
4259
+ }
4260
+ });
3946
4261
  pagesCmd.command("clone <id>").description(
3947
4262
  "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"
3948
4263
  ).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) => {
@@ -4389,6 +4704,59 @@ sentEmailsCmd.command("get <emailID>").description("Read one sent email, includi
4389
4704
  fail(e);
4390
4705
  }
4391
4706
  });
4707
+ var voiceCmd = program.command("voice").description("Read voice agent phone conversations");
4708
+ var voiceCallsCmd = voiceCmd.command("calls").description("Inbound and outbound phone call records");
4709
+ 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(
4710
+ async (opts) => {
4711
+ try {
4712
+ const res = await new ErdoClient().listVoiceCalls({
4713
+ agent: opts.agent,
4714
+ direction: opts.direction,
4715
+ limit: opts.limit ? Number(opts.limit) : void 0,
4716
+ offset: opts.offset ? Number(opts.offset) : void 0,
4717
+ cursor: opts.cursor
4718
+ });
4719
+ if (opts.json) {
4720
+ print(res);
4721
+ return;
4722
+ }
4723
+ const calls = res.conversations ?? [];
4724
+ if (calls.length === 0) {
4725
+ console.log("No calls match those filters.");
4726
+ return;
4727
+ }
4728
+ printAlignedTable(
4729
+ ["call id", "direction", "from", "to", "status", "secs", "transcript", "started", "summary"],
4730
+ calls.map((call) => [
4731
+ call.call_id,
4732
+ call.direction,
4733
+ call.from_number ?? "",
4734
+ call.to_number,
4735
+ call.status,
4736
+ call.duration_seconds,
4737
+ call.has_transcript ? "yes" : "no",
4738
+ call.created_at,
4739
+ call.transcript_summary
4740
+ ])
4741
+ );
4742
+ process.stderr.write(`showing ${calls.length} call(s)
4743
+ `);
4744
+ if (res.next_cursor) {
4745
+ process.stderr.write(`more available \u2014 re-run with --cursor ${res.next_cursor}
4746
+ `);
4747
+ }
4748
+ } catch (e) {
4749
+ fail(e);
4750
+ }
4751
+ }
4752
+ );
4753
+ voiceCallsCmd.command("get <callID>").description("Read one phone call in full: transcript, summary, and per-turn LLM metrics").action(async (callID) => {
4754
+ try {
4755
+ print(await new ErdoClient().getVoiceCall(callID));
4756
+ } catch (e) {
4757
+ fail(e);
4758
+ }
4759
+ });
4392
4760
  var datasetsCmd = program.command("datasets").description("Datasets");
4393
4761
  datasetsCmd.command("list").description("List datasets").option(
4394
4762
  "--class <class>",
@@ -4433,6 +4801,32 @@ datasetsCmd.command("purposes").description(
4433
4801
  fail(e);
4434
4802
  }
4435
4803
  });
4804
+ datasetsCmd.command("set-purpose <slug> [purpose]").description(
4805
+ "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."
4806
+ ).option("--clear", "remove the dataset's purpose instead of setting one").option(
4807
+ "--description <text>",
4808
+ "description for a purpose new to this org; ignored when the org already has an entry for it"
4809
+ ).action(async (slug, purpose, opts) => {
4810
+ try {
4811
+ if (opts.clear && purpose) {
4812
+ fail(new Error("pass a purpose or --clear, not both"));
4813
+ return;
4814
+ }
4815
+ if (!opts.clear && !purpose) {
4816
+ fail(new Error("pass a purpose to set, or --clear to remove one"));
4817
+ return;
4818
+ }
4819
+ const res = await new ErdoClient().setDatasetPurpose(slug, {
4820
+ ...purpose ? { purpose } : {},
4821
+ ...opts.clear ? { clear_purpose: true } : {},
4822
+ ...opts.description ? { purpose_description: opts.description } : {}
4823
+ });
4824
+ for (const w of res.warnings ?? []) console.error(`warning: ${w}`);
4825
+ console.log(res.purpose ? `${slug} ${res.purpose}` : `${slug} (no purpose)`);
4826
+ } catch (e) {
4827
+ fail(e);
4828
+ }
4829
+ });
4436
4830
  datasetsCmd.command("query <slug> <question>").description(
4437
4831
  "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`."
4438
4832
  ).action(async (slug, question) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.68.0",
4
- "description": "Erdo CLI drive datasets, pages, and evals from the terminal or CI",
3
+ "version": "0.72.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"
@@ -53,4 +53,4 @@
53
53
  "overrides": {
54
54
  "esbuild": "^0.28.1"
55
55
  }
56
- }
56
+ }