@erdoai/cli 0.68.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 +296 -15
  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
@@ -434,6 +439,22 @@ var ErdoClient = class {
434
439
  getSentEmail(emailID) {
435
440
  return this.request("GET", `/v1/emails/${encodeURIComponent(emailID)}`);
436
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
+ }
437
458
  // Run a read-only HogQL query against the org's page-analytics events. Rows are
438
459
  // positional per columns; enabled:false means page analytics is off for the org
439
460
  // (not zero traffic). A rejected query surfaces PostHog's message as the error.
@@ -869,6 +890,14 @@ var ErdoClient = class {
869
890
  listDatasetPurposes() {
870
891
  return this.request("GET", `/v1/datasets-purposes`);
871
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
+ }
872
901
  // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
873
902
  // `query` made every `erdo datasets query` fail with "question is required".
874
903
  //
@@ -1165,17 +1194,36 @@ var ErdoClient = class {
1165
1194
  `/v1/kv/${encodeURIComponent(slug)}/items/${encodeURIComponent(key)}`
1166
1195
  );
1167
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
+ }
1168
1214
  // --- agent runs ---
1169
1215
  listAgentRuns(opts) {
1170
1216
  const params = new URLSearchParams();
1171
1217
  if (opts.agent) params.set("agent_key", opts.agent);
1172
1218
  if (opts.thread) params.set("thread_id", opts.thread);
1219
+ if (opts.status) params.set("status", opts.status);
1173
1220
  if (opts.limit) params.set("limit", String(opts.limit));
1174
1221
  const qs = params.toString();
1175
1222
  return this.request("GET", `/v1/runs${qs ? `?${qs}` : ""}`);
1176
1223
  }
1177
- getAgentRun(id) {
1178
- 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}`);
1179
1227
  }
1180
1228
  // --- approvals ---
1181
1229
  listApprovals(opts) {
@@ -1910,9 +1958,20 @@ id: ${o.id}`);
1910
1958
  fail(e);
1911
1959
  }
1912
1960
  });
1913
- 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) => {
1914
1964
  try {
1915
- 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
+ });
1916
1975
  console.log(`Adopted managed org: ${o.name}`);
1917
1976
  console.log(`slug: ${o.slug}
1918
1977
  id: ${o.id}`);
@@ -3019,7 +3078,7 @@ agentCmd.command("messages <threadId>").description("Show a thread's messages (r
3019
3078
  }
3020
3079
  });
3021
3080
  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) => {
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) => {
3023
3082
  try {
3024
3083
  const { runs } = await new ErdoClient().listAgentRuns(opts);
3025
3084
  for (const r of runs) console.log(`${r.id} ${r.status} ${r.agent_key}`);
@@ -3027,13 +3086,130 @@ runsCmd.command("list").description("List agent runs (filter by agent OR thread
3027
3086
  fail(e);
3028
3087
  }
3029
3088
  });
3030
- 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) => {
3031
3090
  try {
3032
- 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);
3033
3100
  } catch (e) {
3034
3101
  fail(e);
3035
3102
  }
3036
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
+ }
3037
3213
  var decisionsCmd = program.command("decisions").description(
3038
3214
  "The decision record \u2014 what your organization committed to, whether the change actually happened, and what the evidence said afterwards"
3039
3215
  );
@@ -3943,6 +4119,32 @@ pagesCmd.command("restore <id>").description("Restore a previously deleted page
3943
4119
  fail(e);
3944
4120
  }
3945
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
+ });
3946
4148
  pagesCmd.command("clone <id>").description(
3947
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"
3948
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) => {
@@ -4389,6 +4591,59 @@ sentEmailsCmd.command("get <emailID>").description("Read one sent email, includi
4389
4591
  fail(e);
4390
4592
  }
4391
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
+ });
4392
4647
  var datasetsCmd = program.command("datasets").description("Datasets");
4393
4648
  datasetsCmd.command("list").description("List datasets").option(
4394
4649
  "--class <class>",
@@ -4433,6 +4688,32 @@ datasetsCmd.command("purposes").description(
4433
4688
  fail(e);
4434
4689
  }
4435
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
+ });
4436
4717
  datasetsCmd.command("query <slug> <question>").description(
4437
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`."
4438
4719
  ).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.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"
@@ -53,4 +53,4 @@
53
53
  "overrides": {
54
54
  "esbuild": "^0.28.1"
55
55
  }
56
- }
56
+ }