@erdoai/cli 0.125.0 → 0.127.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 +63 -10
  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".
@@ -1622,6 +1634,9 @@ var ErdoClient = class {
1622
1634
  getKnowledge(id) {
1623
1635
  return this.request("GET", `/v1/knowledge/${encodeURIComponent(id)}`);
1624
1636
  }
1637
+ createKnowledge(input) {
1638
+ return this.request("POST", "/v1/knowledge", input);
1639
+ }
1625
1640
  updateKnowledge(id, input) {
1626
1641
  return this.request("POST", `/v1/knowledge/${encodeURIComponent(id)}/actions`, {
1627
1642
  action: "update",
@@ -6746,7 +6761,7 @@ datasetsCmd.command("create <name>").description(
6746
6761
  });
6747
6762
  datasetsCmd.command("ensure <purpose>").description(
6748
6763
  "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."
6749
- ).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) => {
6764
+ ).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) => {
6750
6765
  try {
6751
6766
  const declaredSchema = readSchemaFile(opts.schema);
6752
6767
  const result = await new ErdoClient().ensureDataset({
@@ -6755,6 +6770,7 @@ datasetsCmd.command("ensure <purpose>").description(
6755
6770
  ...opts.description ? { description: opts.description } : {},
6756
6771
  ...opts.purposeDescription ? { purpose_description: opts.purposeDescription } : {},
6757
6772
  ...opts.preferredDataset ? { preferred_dataset: opts.preferredDataset } : {},
6773
+ ...opts.reviewedOptionalAddition.length ? { reviewed_optional_additions: opts.reviewedOptionalAddition } : {},
6758
6774
  ...opts.emailColumn ? { email_column: opts.emailColumn } : {},
6759
6775
  ...opts.phoneColumn ? { phone_column: opts.phoneColumn } : {},
6760
6776
  declared_schema: declaredSchema
@@ -7389,10 +7405,10 @@ leadsCmd.command("get <dataset> <lead>").description(
7389
7405
  }
7390
7406
  });
7391
7407
  leadsCmd.command("candidates <dataset>").description(
7392
- "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`."
7393
- ).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) => {
7408
+ "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."
7409
+ ).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) => {
7394
7410
  try {
7395
- const res = await new ErdoClient().listLeadMergeCandidates(dataset, opts.limit);
7411
+ const res = await new ErdoClient().listLeadMergeCandidates(dataset, opts.limit, opts.traffic);
7396
7412
  if (opts.json) {
7397
7413
  print(res);
7398
7414
  return;
@@ -7410,8 +7426,10 @@ leadsCmd.command("candidates <dataset>").description(
7410
7426
  }
7411
7427
  const rows = [];
7412
7428
  for (const item of candidates) {
7429
+ if (!item.candidates?.length) rows.push([item.capture.id, item.capture.created_at, item.capture.resolution, "No candidate", "", "", ""]);
7413
7430
  for (const candidate of item.candidates ?? []) {
7414
7431
  rows.push([
7432
+ item.capture.id,
7415
7433
  item.capture.created_at,
7416
7434
  item.capture.resolution,
7417
7435
  candidate.canonical_lead_id,
@@ -7421,15 +7439,29 @@ leadsCmd.command("candidates <dataset>").description(
7421
7439
  ]);
7422
7440
  }
7423
7441
  }
7424
- printAlignedTable(["captured", "why", "lead", "emails", "phones", "settled"], rows);
7442
+ printAlignedTable(["receipt", "captured", "why", "lead", "emails", "phones", "settled"], rows);
7425
7443
  console.log(
7426
7444
  `
7427
- Merge two of them with: erdo leads merge ${res.dataset_slug} <survivor> --absorb <lead>`
7445
+ Review saved submission evidence with: erdo leads resolve-capture ${res.dataset_slug} <receipt> --input <decision.json>`
7428
7446
  );
7429
7447
  } catch (e) {
7430
7448
  fail(e);
7431
7449
  }
7432
7450
  });
7451
+ 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) => {
7452
+ try {
7453
+ const input = JSON.parse(readFileSync4(opts.input, "utf8"));
7454
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("--input must contain a JSON object");
7455
+ if (Object.hasOwn(input, "apply")) throw new Error("Use the explicit --apply flag; do not put apply in the decision file");
7456
+ for (const key of Object.keys(input)) {
7457
+ if (!["action", "target_lead_id", "reason", "expected_snapshot", "operation_key"].includes(key)) throw new Error(`Unknown decision field: ${key}`);
7458
+ }
7459
+ const result = await new ErdoClient().resolveLeadCapture(dataset, receipt, { ...input, apply: Boolean(opts.apply) });
7460
+ print(result);
7461
+ } catch (e) {
7462
+ fail(e);
7463
+ }
7464
+ });
7433
7465
  leadsCmd.command("merge <dataset> <survivor>").description(
7434
7466
  "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."
7435
7467
  ).requiredOption("--absorb <lead>", "the lead that stops being its own lead (UUID or lead reference)").option(
@@ -7904,6 +7936,10 @@ must not affect priority ${rb.priority.forbidden_factors.join(", ")}`);
7904
7936
  }
7905
7937
  }
7906
7938
  if (rb.flow) printLeadPlaybookFlow(rb.flow);
7939
+ if (rb.call_plan?.length) {
7940
+ console.log("\ncalls to a lead who has not answered (enforced)");
7941
+ for (const line of rb.call_plan) console.log(` ${line}`);
7942
+ }
7907
7943
  if (rb.limits?.length) {
7908
7944
  console.log("\nlimits Erdo applied");
7909
7945
  for (const l of rb.limits) console.log(` ${l.kind} \u2192 ${l.mode}: ${l.reason}`);
@@ -8356,6 +8392,22 @@ pipelinesCmd.command("executions <slug>").description("Show an event pipeline's
8356
8392
  }
8357
8393
  });
8358
8394
  var knowledgeCmd = program.command("knowledge").description("Knowledge objects");
8395
+ knowledgeCmd.command("create").description("Create a Knowledge object").requiredOption("--title <title>", "title").requiredOption("--body <body>", "body text, or @file").option("--description <description>", "short description").option("--type <type>", "snippet or skill", "snippet").option("--category <category>", "category").option("--tags <tags>", "comma-separated tags").option("--agent <name>", "name of the agent making the write").action(async (opts) => {
8396
+ try {
8397
+ const body = readMaybeFile(opts.body) ?? "";
8398
+ print(await new ErdoClient().createKnowledge({
8399
+ title: opts.title,
8400
+ content: body,
8401
+ description: opts.description ?? opts.title,
8402
+ type: opts.type,
8403
+ category: opts.category,
8404
+ tags: opts.tags?.split(",").map((tag) => tag.trim()).filter(Boolean),
8405
+ agent: opts.agent
8406
+ }));
8407
+ } catch (e) {
8408
+ fail(e);
8409
+ }
8410
+ });
8359
8411
  knowledgeCmd.command("get <id>").description("Get one full typed Knowledge object, including links and related objects").action(async (id) => {
8360
8412
  try {
8361
8413
  print(await new ErdoClient().getKnowledge(id));
@@ -8363,7 +8415,7 @@ knowledgeCmd.command("get <id>").description("Get one full typed Knowledge objec
8363
8415
  fail(e);
8364
8416
  }
8365
8417
  });
8366
- knowledgeCmd.command("update <id>").description("Update a Knowledge object while preserving fields omitted from the command").option("--body <value>", "new body text, or @file; pass an empty value to clear the body").option("--config <file>", "JSON object file containing the complete replacement config, or @file").option("--title <title>", "new title").action(async (id, opts) => {
8418
+ knowledgeCmd.command("update <id>").description("Update a Knowledge object while preserving fields omitted from the command").option("--body <value>", "new body text, or @file; pass an empty value to clear the body").option("--config <file>", "JSON object file containing the complete replacement config, or @file").option("--title <title>", "new title").option("--agent <name>", "name of the agent making the write").action(async (id, opts) => {
8367
8419
  try {
8368
8420
  if (opts.title === void 0 && opts.body === void 0 && opts.config === void 0) {
8369
8421
  throw new Error("provide at least one of --title, --body, or --config");
@@ -8380,7 +8432,8 @@ knowledgeCmd.command("update <id>").description("Update a Knowledge object while
8380
8432
  title: opts.title ?? object.title,
8381
8433
  body_markdown: replacementBody ?? object.body_markdown,
8382
8434
  config_json: replacementConfig ?? object.config_json,
8383
- expected_updated_at: object.updated_at
8435
+ expected_updated_at: object.updated_at,
8436
+ agent: opts.agent
8384
8437
  }));
8385
8438
  } catch (e) {
8386
8439
  fail(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.125.0",
3
+ "version": "0.127.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {