@erdoai/cli 0.126.0 → 0.128.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 +44 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1257,10 +1257,22 @@ var ErdoClient = class {
1257
1257
  }
1258
1258
  // The captures the platform refused to resolve: two leads sharing a contact
1259
1259
  // detail, or a submission whose email and phone point at different leads.
1260
- listLeadMergeCandidates(datasetSlug, limit) {
1261
- const qs = limit ? `?limit=${encodeURIComponent(String(limit))}` : "";
1260
+ listLeadMergeCandidates(datasetSlug, limit, traffic) {
1261
+ const query = new URLSearchParams();
1262
+ if (limit !== void 0) query.set("limit", String(limit));
1263
+ if (traffic !== void 0) query.set("traffic", traffic);
1264
+ const qs = query.size ? `?${query}` : "";
1262
1265
  return this.request("GET", `/v1/datasets/${encodeURIComponent(datasetSlug)}/lead-candidates${qs}`);
1263
1266
  }
1267
+ resolveLeadCapture(datasetSlug, captureID, input) {
1268
+ if (!["apply_to_lead", "dismiss"].includes(input.action) || typeof input.reason !== "string" || !input.reason.trim()) {
1269
+ throw new Error("A resolution action and reason are required.");
1270
+ }
1271
+ if (input.apply && (!input.operation_key || !input.expected_snapshot)) {
1272
+ throw new Error("Apply requires operation_key and expected_snapshot from the exact preview.");
1273
+ }
1274
+ return this.request("POST", `/v1/datasets/${encodeURIComponent(datasetSlug)}/lead-captures/${encodeURIComponent(captureID)}/resolve`, input);
1275
+ }
1264
1276
  // Not reversible in practice: the absorbed lead's row values are combined into
1265
1277
  // the survivor's and its row is gone. It needs no idempotency key — the pair it
1266
1278
  // joins identifies it, so a retry answers "already_merged".
@@ -1784,6 +1796,7 @@ var ErdoClient = class {
1784
1796
  if (opts.applicability) params.set("applicability", opts.applicability);
1785
1797
  if (opts.outcome) params.set("outcome", opts.outcome);
1786
1798
  if (opts.approval_id) params.set("approval_id", opts.approval_id);
1799
+ if (opts.since) params.set("since", opts.since);
1787
1800
  if (opts.limit) params.set("limit", String(opts.limit));
1788
1801
  if (opts.offset) params.set("offset", String(opts.offset));
1789
1802
  const qs = params.toString();
@@ -4397,7 +4410,7 @@ decisionsCmd.command("list").description("List decisions, newest first").option(
4397
4410
  ).option("--applicability <kind>", "standing | one_shot").option("--outcome <outcome>", "only decisions with a settled effect that came back met | not_met | inconclusive").option(
4398
4411
  "--approval <id>",
4399
4412
  "only the decisions produced by answering this approval (approvals are referenced by id, not slug)"
4400
- ).option("-l, --limit <n>", "max rows (default 50, max 200)", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).option("--json", "output raw JSON").action(
4413
+ ).option("--since <timestamp>", "inclusive RFC3339 lower bound on the latest known decision movement").option("-l, --limit <n>", "max rows (default 50, max 200)", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).option("--json", "output raw JSON").action(
4401
4414
  async (opts) => {
4402
4415
  try {
4403
4416
  const res = await new ErdoClient().listDecisions({
@@ -4411,6 +4424,7 @@ decisionsCmd.command("list").description("List decisions, newest first").option(
4411
4424
  applicability: opts.applicability,
4412
4425
  outcome: opts.outcome,
4413
4426
  approval_id: opts.approval,
4427
+ since: opts.since,
4414
4428
  limit: opts.limit,
4415
4429
  offset: opts.offset
4416
4430
  });
@@ -6749,7 +6763,7 @@ datasetsCmd.command("create <name>").description(
6749
6763
  });
6750
6764
  datasetsCmd.command("ensure <purpose>").description(
6751
6765
  "Create or reuse the one dataset for a purpose and verify its declared producer contract. Refuses legacy splits and incompatible populated datasets instead of choosing silently."
6752
- ).requiredOption("--schema <file>", "JSON file containing { columns: [...] } or a columns array").option("--name <name>", "dataset name when creation is required").option("--description <text>", "dataset description when creation is required").option("--purpose-description <text>", "what writes and reads this purpose").option("--preferred-dataset <slug>", "existing bound dataset slug or id to adopt").option("--email-column <name>", "for a leads purpose: which declared column holds the email (default email)").option("--phone-column <name>", "for a leads purpose: which declared column holds the phone (default phone)").action(async (purpose, opts) => {
6766
+ ).requiredOption("--schema <file>", "JSON file containing { columns: [...] } or a columns array").option("--name <name>", "dataset name when creation is required").option("--description <text>", "dataset description when creation is required").option("--purpose-description <text>", "what writes and reads this purpose").option("--preferred-dataset <slug>", "existing bound dataset slug or id to adopt").option("--reviewed-optional-addition <name>", "producer-reviewed optional column to add atomically; repeat for multiple columns", (value, previous = []) => [...previous, value], []).option("--email-column <name>", "for a leads purpose: which declared column holds the email (default email)").option("--phone-column <name>", "for a leads purpose: which declared column holds the phone (default phone)").action(async (purpose, opts) => {
6753
6767
  try {
6754
6768
  const declaredSchema = readSchemaFile(opts.schema);
6755
6769
  const result = await new ErdoClient().ensureDataset({
@@ -6758,6 +6772,7 @@ datasetsCmd.command("ensure <purpose>").description(
6758
6772
  ...opts.description ? { description: opts.description } : {},
6759
6773
  ...opts.purposeDescription ? { purpose_description: opts.purposeDescription } : {},
6760
6774
  ...opts.preferredDataset ? { preferred_dataset: opts.preferredDataset } : {},
6775
+ ...opts.reviewedOptionalAddition.length ? { reviewed_optional_additions: opts.reviewedOptionalAddition } : {},
6761
6776
  ...opts.emailColumn ? { email_column: opts.emailColumn } : {},
6762
6777
  ...opts.phoneColumn ? { phone_column: opts.phoneColumn } : {},
6763
6778
  declared_schema: declaredSchema
@@ -7392,10 +7407,10 @@ leadsCmd.command("get <dataset> <lead>").description(
7392
7407
  }
7393
7408
  });
7394
7409
  leadsCmd.command("candidates <dataset>").description(
7395
- "List the captures the platform refused to resolve \u2014 where two leads share a contact detail, or a submission's email and phone point at different leads. The platform records the candidates instead of guessing, and this is the queue a person drains with `leads merge`."
7396
- ).option("-l, --limit <n>", "maximum captures to return (default 50, maximum 200)", (v) => parseInt(v, 10)).option("--json", "print the raw JSON result instead of a table").action(async (dataset, opts) => {
7410
+ "List unresolved refused submissions and their candidate leads. Inspect the saved evidence before previewing an apply-to-lead or dismissal decision with leads resolve-capture."
7411
+ ).option("-l, --limit <n>", "maximum captures to return (default 50, maximum 200)", (v) => parseInt(v, 10)).addOption(new Option2("--traffic <kind>", "include real submissions, independently trusted tests, or both").choices(["real", "test", "all"])).option("--json", "print the raw JSON result instead of a table").action(async (dataset, opts) => {
7397
7412
  try {
7398
- const res = await new ErdoClient().listLeadMergeCandidates(dataset, opts.limit);
7413
+ const res = await new ErdoClient().listLeadMergeCandidates(dataset, opts.limit, opts.traffic);
7399
7414
  if (opts.json) {
7400
7415
  print(res);
7401
7416
  return;
@@ -7413,8 +7428,10 @@ leadsCmd.command("candidates <dataset>").description(
7413
7428
  }
7414
7429
  const rows = [];
7415
7430
  for (const item of candidates) {
7431
+ if (!item.candidates?.length) rows.push([item.capture.id, item.capture.created_at, item.capture.resolution, "No candidate", "", "", ""]);
7416
7432
  for (const candidate of item.candidates ?? []) {
7417
7433
  rows.push([
7434
+ item.capture.id,
7418
7435
  item.capture.created_at,
7419
7436
  item.capture.resolution,
7420
7437
  candidate.canonical_lead_id,
@@ -7424,15 +7441,29 @@ leadsCmd.command("candidates <dataset>").description(
7424
7441
  ]);
7425
7442
  }
7426
7443
  }
7427
- printAlignedTable(["captured", "why", "lead", "emails", "phones", "settled"], rows);
7444
+ printAlignedTable(["receipt", "captured", "why", "lead", "emails", "phones", "settled"], rows);
7428
7445
  console.log(
7429
7446
  `
7430
- Merge two of them with: erdo leads merge ${res.dataset_slug} <survivor> --absorb <lead>`
7447
+ Review saved submission evidence with: erdo leads resolve-capture ${res.dataset_slug} <receipt> --input <decision.json>`
7431
7448
  );
7432
7449
  } catch (e) {
7433
7450
  fail(e);
7434
7451
  }
7435
7452
  });
7453
+ leadsCmd.command("resolve-capture <dataset> <receipt>").description("Preview a refused submission against a candidate lead, or record an audited dismissal. Applying preserves the original receipt and does not merge identities, report a conversion, or replay arrival automations.").requiredOption("--input <file>", "JSON decision: action, target_lead_id for apply_to_lead, reason; applying also requires expected_snapshot and operation_key").option("--apply", "confirm the exact preview; without this flag the decision only previews").action(async (dataset, receipt, opts) => {
7454
+ try {
7455
+ const input = JSON.parse(readFileSync4(opts.input, "utf8"));
7456
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("--input must contain a JSON object");
7457
+ if (Object.hasOwn(input, "apply")) throw new Error("Use the explicit --apply flag; do not put apply in the decision file");
7458
+ for (const key of Object.keys(input)) {
7459
+ if (!["action", "target_lead_id", "reason", "expected_snapshot", "operation_key"].includes(key)) throw new Error(`Unknown decision field: ${key}`);
7460
+ }
7461
+ const result = await new ErdoClient().resolveLeadCapture(dataset, receipt, { ...input, apply: Boolean(opts.apply) });
7462
+ print(result);
7463
+ } catch (e) {
7464
+ fail(e);
7465
+ }
7466
+ });
7436
7467
  leadsCmd.command("merge <dataset> <survivor>").description(
7437
7468
  "Merge two leads into one. The absorbed lead stops being its own lead: its contact evidence moves onto the survivor, and the two dataset rows become one row \u2014 a value the absorbed row carries fills a blank in the survivor's row, and never replaces one the survivor already holds unless you name the column with --overwrite-column. This cannot be undone: the absorbed row's values are combined away and only the pointer survives, so an id already handed out keeps resolving. Read both leads with `leads get` before running it."
7438
7469
  ).requiredOption("--absorb <lead>", "the lead that stops being its own lead (UUID or lead reference)").option(
@@ -7907,6 +7938,10 @@ must not affect priority ${rb.priority.forbidden_factors.join(", ")}`);
7907
7938
  }
7908
7939
  }
7909
7940
  if (rb.flow) printLeadPlaybookFlow(rb.flow);
7941
+ if (rb.call_plan?.length) {
7942
+ console.log("\ncalls to a lead who has not answered (enforced)");
7943
+ for (const line of rb.call_plan) console.log(` ${line}`);
7944
+ }
7910
7945
  if (rb.limits?.length) {
7911
7946
  console.log("\nlimits Erdo applied");
7912
7947
  for (const l of rb.limits) console.log(` ${l.kind} \u2192 ${l.mode}: ${l.reason}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.126.0",
3
+ "version": "0.128.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {