@erdoai/cli 0.87.0 → 0.88.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.
- package/dist/index.js +171 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1094,6 +1094,33 @@ var ErdoClient = class {
|
|
|
1094
1094
|
// that was never live (an upload merged into an existing table); file_type may be
|
|
1095
1095
|
// absent when the stored file's type was never recorded. Read one back by passing
|
|
1096
1096
|
// its id as revision_id to fetchDatasetContents.
|
|
1097
|
+
// ── leads ──────────────────────────────────────────────────────────────────
|
|
1098
|
+
// Permanent lead identity. A lead is addressed by its canonical_lead_id or by
|
|
1099
|
+
// the 22-character reference short enough for a link or an SMS; an id that has
|
|
1100
|
+
// since been merged away resolves to the lead it became, so an id read off an
|
|
1101
|
+
// old capture response keeps working.
|
|
1102
|
+
getLead(datasetSlug, lead) {
|
|
1103
|
+
return this.request(
|
|
1104
|
+
"GET",
|
|
1105
|
+
`/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(lead)}`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
// The captures the platform refused to resolve: two leads sharing a contact
|
|
1109
|
+
// detail, or a submission whose email and phone point at different leads.
|
|
1110
|
+
listLeadMergeCandidates(datasetSlug, limit) {
|
|
1111
|
+
const qs = limit ? `?limit=${encodeURIComponent(String(limit))}` : "";
|
|
1112
|
+
return this.request("GET", `/v1/datasets/${encodeURIComponent(datasetSlug)}/lead-candidates${qs}`);
|
|
1113
|
+
}
|
|
1114
|
+
// Not reversible in practice: the absorbed lead's row values are combined into
|
|
1115
|
+
// the survivor's and its row is gone. It needs no idempotency key — the pair it
|
|
1116
|
+
// joins identifies it, so a retry answers "already_merged".
|
|
1117
|
+
mergeLeads(datasetSlug, survivor, input) {
|
|
1118
|
+
return this.request(
|
|
1119
|
+
"POST",
|
|
1120
|
+
`/v1/datasets/${encodeURIComponent(datasetSlug)}/leads/${encodeURIComponent(survivor)}/merge`,
|
|
1121
|
+
input
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1097
1124
|
listDatasetRevisions(slug) {
|
|
1098
1125
|
return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
|
|
1099
1126
|
}
|
|
@@ -1750,6 +1777,16 @@ async function resolveSecretInput(options) {
|
|
|
1750
1777
|
}
|
|
1751
1778
|
return void 0;
|
|
1752
1779
|
}
|
|
1780
|
+
async function promptLine(question) {
|
|
1781
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return "";
|
|
1782
|
+
const readline2 = await import("readline/promises");
|
|
1783
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1784
|
+
try {
|
|
1785
|
+
return await rl.question(question);
|
|
1786
|
+
} finally {
|
|
1787
|
+
rl.close();
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1753
1790
|
|
|
1754
1791
|
// src/integrations.ts
|
|
1755
1792
|
function formatIntegrationListRow(i) {
|
|
@@ -6073,6 +6110,140 @@ filterCmd.command("list <slug>").description("List the default filters on a data
|
|
|
6073
6110
|
fail(e);
|
|
6074
6111
|
}
|
|
6075
6112
|
});
|
|
6113
|
+
var leadsCmd = program.command("leads").description("Read leads in a lead dataset, and merge two that are the same person");
|
|
6114
|
+
leadsCmd.command("get <dataset> <lead>").description(
|
|
6115
|
+
"Read one lead: its permanent canonical_lead_id, the email and phone evidence bound to it, its dataset row, the leads merged into it, and its capture history. <lead> is either the canonical_lead_id UUID or the 22-character lead reference, and an id that has since been merged away resolves to the lead it became \u2014 so an id read off an old capture response still works."
|
|
6116
|
+
).option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
|
|
6117
|
+
try {
|
|
6118
|
+
const res = await new ErdoClient().getLead(dataset, lead);
|
|
6119
|
+
if (opts.json) {
|
|
6120
|
+
print(res);
|
|
6121
|
+
return;
|
|
6122
|
+
}
|
|
6123
|
+
printLead(res.lead);
|
|
6124
|
+
} catch (e) {
|
|
6125
|
+
fail(e);
|
|
6126
|
+
}
|
|
6127
|
+
});
|
|
6128
|
+
leadsCmd.command("candidates <dataset>").description(
|
|
6129
|
+
"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`."
|
|
6130
|
+
).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) => {
|
|
6131
|
+
try {
|
|
6132
|
+
const res = await new ErdoClient().listLeadMergeCandidates(dataset, opts.limit);
|
|
6133
|
+
if (opts.json) {
|
|
6134
|
+
print(res);
|
|
6135
|
+
return;
|
|
6136
|
+
}
|
|
6137
|
+
const candidates = res.candidates ?? [];
|
|
6138
|
+
if (!res.lead_identity_enabled) {
|
|
6139
|
+
console.log(
|
|
6140
|
+
`${res.dataset_slug} does not have permanent lead identity, so nothing has been checked for duplicates. Migrate the dataset first.`
|
|
6141
|
+
);
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
6144
|
+
if (candidates.length === 0) {
|
|
6145
|
+
console.log(`${res.dataset_slug} has no unresolved lead captures.`);
|
|
6146
|
+
return;
|
|
6147
|
+
}
|
|
6148
|
+
const rows = [];
|
|
6149
|
+
for (const item of candidates) {
|
|
6150
|
+
for (const candidate of item.candidates ?? []) {
|
|
6151
|
+
rows.push([
|
|
6152
|
+
item.capture.created_at,
|
|
6153
|
+
item.capture.resolution,
|
|
6154
|
+
candidate.canonical_lead_id,
|
|
6155
|
+
(candidate.emails ?? []).join(" "),
|
|
6156
|
+
(candidate.phones ?? []).join(" "),
|
|
6157
|
+
candidate.merged_into ? `merged into ${candidate.merged_into}` : ""
|
|
6158
|
+
]);
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
printAlignedTable(["captured", "why", "lead", "emails", "phones", "settled"], rows);
|
|
6162
|
+
console.log(
|
|
6163
|
+
`
|
|
6164
|
+
Merge two of them with: erdo leads merge ${res.dataset_slug} <survivor> --absorb <lead>`
|
|
6165
|
+
);
|
|
6166
|
+
} catch (e) {
|
|
6167
|
+
fail(e);
|
|
6168
|
+
}
|
|
6169
|
+
});
|
|
6170
|
+
leadsCmd.command("merge <dataset> <survivor>").description(
|
|
6171
|
+
"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."
|
|
6172
|
+
).requiredOption("--absorb <lead>", "the lead that stops being its own lead (UUID or lead reference)").option(
|
|
6173
|
+
"--overwrite-column <name>",
|
|
6174
|
+
"a column where the absorbed lead's value wins over one the survivor already holds; repeatable",
|
|
6175
|
+
(value, previous) => [...previous, value],
|
|
6176
|
+
[]
|
|
6177
|
+
).option("-y, --yes", "skip the confirmation prompt").option("--json", "print the raw JSON result instead of a summary").action(
|
|
6178
|
+
async (dataset, survivor, opts) => {
|
|
6179
|
+
try {
|
|
6180
|
+
if (!opts.yes) {
|
|
6181
|
+
const answer = await promptLine(
|
|
6182
|
+
`Merge ${opts.absorb} into ${survivor} in ${dataset}? The absorbed lead's row is combined away and this cannot be undone. Type "merge" to continue: `
|
|
6183
|
+
);
|
|
6184
|
+
if (answer.trim() !== "merge") {
|
|
6185
|
+
console.error("Not merged.");
|
|
6186
|
+
process.exit(1);
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
const res = await new ErdoClient().mergeLeads(dataset, survivor, {
|
|
6190
|
+
absorb_lead_id: opts.absorb,
|
|
6191
|
+
overwrite_columns: opts.overwriteColumn.length ? opts.overwriteColumn : void 0
|
|
6192
|
+
});
|
|
6193
|
+
if (opts.json) {
|
|
6194
|
+
print(res);
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6197
|
+
if (res.state === "already_merged") {
|
|
6198
|
+
console.log(`Already one lead: ${res.absorbed_lead_id} resolves to ${res.lead.canonical_lead_id}.`);
|
|
6199
|
+
} else {
|
|
6200
|
+
console.log(
|
|
6201
|
+
`Merged ${res.absorbed_lead_id} into ${res.lead.canonical_lead_id} \u2014 ${res.aliases_moved} contact ${res.aliases_moved === 1 ? "alias" : "aliases"} moved, ${res.rows_combined} ${res.rows_combined === 1 ? "row" : "rows"} combined.`
|
|
6202
|
+
);
|
|
6203
|
+
}
|
|
6204
|
+
printLead(res.lead);
|
|
6205
|
+
} catch (e) {
|
|
6206
|
+
fail(e);
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
);
|
|
6210
|
+
function printLead(lead) {
|
|
6211
|
+
console.log(`lead ${lead.canonical_lead_id}`);
|
|
6212
|
+
console.log(`reference ${lead.reference}`);
|
|
6213
|
+
console.log(`dataset ${lead.dataset_slug}`);
|
|
6214
|
+
if (lead.requested_lead_id && lead.requested_lead_id !== lead.canonical_lead_id) {
|
|
6215
|
+
console.log(`asked for ${lead.requested_lead_id} (merged into this lead)`);
|
|
6216
|
+
}
|
|
6217
|
+
if (lead.merged_into) {
|
|
6218
|
+
console.log(`merged into ${lead.merged_into}${lead.merged_at ? ` on ${lead.merged_at}` : ""}`);
|
|
6219
|
+
}
|
|
6220
|
+
if (lead.absorbed_lead_ids?.length) {
|
|
6221
|
+
console.log(`absorbed ${lead.absorbed_lead_ids.join(", ")}`);
|
|
6222
|
+
}
|
|
6223
|
+
if (lead.aliases?.length) {
|
|
6224
|
+
console.log("\ncontact evidence");
|
|
6225
|
+
printAlignedTable(
|
|
6226
|
+
["type", "value", "from"],
|
|
6227
|
+
lead.aliases.map((a) => [a.type, a.value, a.provenance])
|
|
6228
|
+
);
|
|
6229
|
+
}
|
|
6230
|
+
if (lead.captures?.length) {
|
|
6231
|
+
console.log("\ncaptures");
|
|
6232
|
+
printAlignedTable(
|
|
6233
|
+
["when", "producer", "resolution", "conversion"],
|
|
6234
|
+
lead.captures.map((c) => [c.created_at, c.producer, c.resolution, c.conversion_status])
|
|
6235
|
+
);
|
|
6236
|
+
}
|
|
6237
|
+
if (lead.row) {
|
|
6238
|
+
console.log("\nrow");
|
|
6239
|
+
printAlignedTable(
|
|
6240
|
+
["column", "value"],
|
|
6241
|
+
Object.keys(lead.row).sort().map((column) => [column, lead.row?.[column] ?? ""])
|
|
6242
|
+
);
|
|
6243
|
+
} else {
|
|
6244
|
+
console.log("\nThis lead holds no dataset row \u2014 its values were combined into the lead it was merged with.");
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6076
6247
|
var integrationsCmd = program.command("integrations").description("Connect and inspect integrations");
|
|
6077
6248
|
integrationsCmd.command("list").description("List connected integrations \u2014 app, status, auth type, name, then the connection's id (what --from-connection takes)").action(async () => {
|
|
6078
6249
|
try {
|