@erdoai/cli 0.87.0 → 0.89.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 +231 -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
|
}
|
|
@@ -1126,6 +1153,23 @@ var ErdoClient = class {
|
|
|
1126
1153
|
`/v1/datasets/${encodeURIComponent(slug)}/refresh/run`
|
|
1127
1154
|
);
|
|
1128
1155
|
}
|
|
1156
|
+
// --- lead identity ---
|
|
1157
|
+
// Preview or perform a dataset's permanent-lead-identity migration: give every
|
|
1158
|
+
// pipeline writer the producer contract, backfill the IDs, and publish the
|
|
1159
|
+
// identity config in one transaction.
|
|
1160
|
+
//
|
|
1161
|
+
// apply:false mutates nothing and is the way to read readiness — `ready`, the
|
|
1162
|
+
// `blockers` holding it up, and how many rows would be assigned an identity.
|
|
1163
|
+
// apply:true needs an operation_key: replaying the same key settles an apply
|
|
1164
|
+
// whose response was lost, while a different key against a migrated dataset is
|
|
1165
|
+
// refused rather than migrating twice.
|
|
1166
|
+
maintainLeadIdentity(slug, body) {
|
|
1167
|
+
return this.request(
|
|
1168
|
+
"POST",
|
|
1169
|
+
`/v1/datasets/${encodeURIComponent(slug)}/lead-identity/maintain`,
|
|
1170
|
+
body
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1129
1173
|
// --- bounded outreach ---
|
|
1130
1174
|
putOutreachBatch(batch, body) {
|
|
1131
1175
|
return this.request(
|
|
@@ -1750,6 +1794,16 @@ async function resolveSecretInput(options) {
|
|
|
1750
1794
|
}
|
|
1751
1795
|
return void 0;
|
|
1752
1796
|
}
|
|
1797
|
+
async function promptLine(question) {
|
|
1798
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return "";
|
|
1799
|
+
const readline2 = await import("readline/promises");
|
|
1800
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1801
|
+
try {
|
|
1802
|
+
return await rl.question(question);
|
|
1803
|
+
} finally {
|
|
1804
|
+
rl.close();
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1753
1807
|
|
|
1754
1808
|
// src/integrations.ts
|
|
1755
1809
|
function formatIntegrationListRow(i) {
|
|
@@ -5616,6 +5670,49 @@ datasetsCmd.command("set-purpose <slug> [purpose]").description(
|
|
|
5616
5670
|
fail(e);
|
|
5617
5671
|
}
|
|
5618
5672
|
});
|
|
5673
|
+
var leadIdentityCmd = datasetsCmd.command("lead-identity").description(
|
|
5674
|
+
"Permanent lead identity \u2014 one lead is one row, however many ways they got in touch. Migrating a dataset gives every row a canonical_lead_id, points every pipeline writer at it, and makes the resolver the only thing that decides whether a submission is a new lead or an existing one."
|
|
5675
|
+
);
|
|
5676
|
+
leadIdentityCmd.command("maintain <slug>").description(
|
|
5677
|
+
"Preview or perform the migration for one dataset. Without --apply it mutates nothing and reports readiness: whether the dataset is ready, what blocks it, and how many rows would be assigned an identity. Run the preview first and read the blockers \u2014 apply refuses outright while any consumer is incompatible, unverifiable, queued or running, because the backfill rewrites the file every one of them reads."
|
|
5678
|
+
).option(
|
|
5679
|
+
"--apply",
|
|
5680
|
+
"perform the migration rather than preview it; requires --operation-key"
|
|
5681
|
+
).option(
|
|
5682
|
+
"--operation-key <key>",
|
|
5683
|
+
"the migration receipt. Replaying the same key settles an apply whose response was lost; a different key against an already-migrated dataset is refused rather than migrating it twice."
|
|
5684
|
+
).option("--email-column <column>", "the dataset's email column (detected when omitted)").option("--phone-column <column>", "the dataset's phone column (detected when omitted)").option(
|
|
5685
|
+
"--conversion-id-column <column>",
|
|
5686
|
+
"optional conversion id column; must differ from both contact columns"
|
|
5687
|
+
).option(
|
|
5688
|
+
"--default-calling-code <code>",
|
|
5689
|
+
"calling code a phone written in national form belongs to, so one number written two ways is one lead (default 1)"
|
|
5690
|
+
).action(
|
|
5691
|
+
async (slug, opts) => {
|
|
5692
|
+
try {
|
|
5693
|
+
if (opts.apply && !opts.operationKey) {
|
|
5694
|
+
fail(
|
|
5695
|
+
new Error(
|
|
5696
|
+
"--apply requires --operation-key so an uncertain retry can settle rather than migrating twice"
|
|
5697
|
+
)
|
|
5698
|
+
);
|
|
5699
|
+
return;
|
|
5700
|
+
}
|
|
5701
|
+
print(
|
|
5702
|
+
await new ErdoClient().maintainLeadIdentity(slug, {
|
|
5703
|
+
apply: Boolean(opts.apply),
|
|
5704
|
+
operation_key: opts.operationKey,
|
|
5705
|
+
email_column: opts.emailColumn,
|
|
5706
|
+
phone_column: opts.phoneColumn,
|
|
5707
|
+
conversion_id_column: opts.conversionIdColumn,
|
|
5708
|
+
default_calling_code: opts.defaultCallingCode
|
|
5709
|
+
})
|
|
5710
|
+
);
|
|
5711
|
+
} catch (e) {
|
|
5712
|
+
fail(e);
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5715
|
+
);
|
|
5619
5716
|
datasetsCmd.command("query <slug> <question>").description(
|
|
5620
5717
|
"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`."
|
|
5621
5718
|
).action(async (slug, question) => {
|
|
@@ -6073,6 +6170,140 @@ filterCmd.command("list <slug>").description("List the default filters on a data
|
|
|
6073
6170
|
fail(e);
|
|
6074
6171
|
}
|
|
6075
6172
|
});
|
|
6173
|
+
var leadsCmd = program.command("leads").description("Read leads in a lead dataset, and merge two that are the same person");
|
|
6174
|
+
leadsCmd.command("get <dataset> <lead>").description(
|
|
6175
|
+
"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."
|
|
6176
|
+
).option("--json", "print the raw JSON result instead of a summary").action(async (dataset, lead, opts) => {
|
|
6177
|
+
try {
|
|
6178
|
+
const res = await new ErdoClient().getLead(dataset, lead);
|
|
6179
|
+
if (opts.json) {
|
|
6180
|
+
print(res);
|
|
6181
|
+
return;
|
|
6182
|
+
}
|
|
6183
|
+
printLead(res.lead);
|
|
6184
|
+
} catch (e) {
|
|
6185
|
+
fail(e);
|
|
6186
|
+
}
|
|
6187
|
+
});
|
|
6188
|
+
leadsCmd.command("candidates <dataset>").description(
|
|
6189
|
+
"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`."
|
|
6190
|
+
).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) => {
|
|
6191
|
+
try {
|
|
6192
|
+
const res = await new ErdoClient().listLeadMergeCandidates(dataset, opts.limit);
|
|
6193
|
+
if (opts.json) {
|
|
6194
|
+
print(res);
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6197
|
+
const candidates = res.candidates ?? [];
|
|
6198
|
+
if (!res.lead_identity_enabled) {
|
|
6199
|
+
console.log(
|
|
6200
|
+
`${res.dataset_slug} does not have permanent lead identity, so nothing has been checked for duplicates. Migrate the dataset first.`
|
|
6201
|
+
);
|
|
6202
|
+
return;
|
|
6203
|
+
}
|
|
6204
|
+
if (candidates.length === 0) {
|
|
6205
|
+
console.log(`${res.dataset_slug} has no unresolved lead captures.`);
|
|
6206
|
+
return;
|
|
6207
|
+
}
|
|
6208
|
+
const rows = [];
|
|
6209
|
+
for (const item of candidates) {
|
|
6210
|
+
for (const candidate of item.candidates ?? []) {
|
|
6211
|
+
rows.push([
|
|
6212
|
+
item.capture.created_at,
|
|
6213
|
+
item.capture.resolution,
|
|
6214
|
+
candidate.canonical_lead_id,
|
|
6215
|
+
(candidate.emails ?? []).join(" "),
|
|
6216
|
+
(candidate.phones ?? []).join(" "),
|
|
6217
|
+
candidate.merged_into ? `merged into ${candidate.merged_into}` : ""
|
|
6218
|
+
]);
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
printAlignedTable(["captured", "why", "lead", "emails", "phones", "settled"], rows);
|
|
6222
|
+
console.log(
|
|
6223
|
+
`
|
|
6224
|
+
Merge two of them with: erdo leads merge ${res.dataset_slug} <survivor> --absorb <lead>`
|
|
6225
|
+
);
|
|
6226
|
+
} catch (e) {
|
|
6227
|
+
fail(e);
|
|
6228
|
+
}
|
|
6229
|
+
});
|
|
6230
|
+
leadsCmd.command("merge <dataset> <survivor>").description(
|
|
6231
|
+
"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."
|
|
6232
|
+
).requiredOption("--absorb <lead>", "the lead that stops being its own lead (UUID or lead reference)").option(
|
|
6233
|
+
"--overwrite-column <name>",
|
|
6234
|
+
"a column where the absorbed lead's value wins over one the survivor already holds; repeatable",
|
|
6235
|
+
(value, previous) => [...previous, value],
|
|
6236
|
+
[]
|
|
6237
|
+
).option("-y, --yes", "skip the confirmation prompt").option("--json", "print the raw JSON result instead of a summary").action(
|
|
6238
|
+
async (dataset, survivor, opts) => {
|
|
6239
|
+
try {
|
|
6240
|
+
if (!opts.yes) {
|
|
6241
|
+
const answer = await promptLine(
|
|
6242
|
+
`Merge ${opts.absorb} into ${survivor} in ${dataset}? The absorbed lead's row is combined away and this cannot be undone. Type "merge" to continue: `
|
|
6243
|
+
);
|
|
6244
|
+
if (answer.trim() !== "merge") {
|
|
6245
|
+
console.error("Not merged.");
|
|
6246
|
+
process.exit(1);
|
|
6247
|
+
}
|
|
6248
|
+
}
|
|
6249
|
+
const res = await new ErdoClient().mergeLeads(dataset, survivor, {
|
|
6250
|
+
absorb_lead_id: opts.absorb,
|
|
6251
|
+
overwrite_columns: opts.overwriteColumn.length ? opts.overwriteColumn : void 0
|
|
6252
|
+
});
|
|
6253
|
+
if (opts.json) {
|
|
6254
|
+
print(res);
|
|
6255
|
+
return;
|
|
6256
|
+
}
|
|
6257
|
+
if (res.state === "already_merged") {
|
|
6258
|
+
console.log(`Already one lead: ${res.absorbed_lead_id} resolves to ${res.lead.canonical_lead_id}.`);
|
|
6259
|
+
} else {
|
|
6260
|
+
console.log(
|
|
6261
|
+
`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.`
|
|
6262
|
+
);
|
|
6263
|
+
}
|
|
6264
|
+
printLead(res.lead);
|
|
6265
|
+
} catch (e) {
|
|
6266
|
+
fail(e);
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
);
|
|
6270
|
+
function printLead(lead) {
|
|
6271
|
+
console.log(`lead ${lead.canonical_lead_id}`);
|
|
6272
|
+
console.log(`reference ${lead.reference}`);
|
|
6273
|
+
console.log(`dataset ${lead.dataset_slug}`);
|
|
6274
|
+
if (lead.requested_lead_id && lead.requested_lead_id !== lead.canonical_lead_id) {
|
|
6275
|
+
console.log(`asked for ${lead.requested_lead_id} (merged into this lead)`);
|
|
6276
|
+
}
|
|
6277
|
+
if (lead.merged_into) {
|
|
6278
|
+
console.log(`merged into ${lead.merged_into}${lead.merged_at ? ` on ${lead.merged_at}` : ""}`);
|
|
6279
|
+
}
|
|
6280
|
+
if (lead.absorbed_lead_ids?.length) {
|
|
6281
|
+
console.log(`absorbed ${lead.absorbed_lead_ids.join(", ")}`);
|
|
6282
|
+
}
|
|
6283
|
+
if (lead.aliases?.length) {
|
|
6284
|
+
console.log("\ncontact evidence");
|
|
6285
|
+
printAlignedTable(
|
|
6286
|
+
["type", "value", "from"],
|
|
6287
|
+
lead.aliases.map((a) => [a.type, a.value, a.provenance])
|
|
6288
|
+
);
|
|
6289
|
+
}
|
|
6290
|
+
if (lead.captures?.length) {
|
|
6291
|
+
console.log("\ncaptures");
|
|
6292
|
+
printAlignedTable(
|
|
6293
|
+
["when", "producer", "resolution", "conversion"],
|
|
6294
|
+
lead.captures.map((c) => [c.created_at, c.producer, c.resolution, c.conversion_status])
|
|
6295
|
+
);
|
|
6296
|
+
}
|
|
6297
|
+
if (lead.row) {
|
|
6298
|
+
console.log("\nrow");
|
|
6299
|
+
printAlignedTable(
|
|
6300
|
+
["column", "value"],
|
|
6301
|
+
Object.keys(lead.row).sort().map((column) => [column, lead.row?.[column] ?? ""])
|
|
6302
|
+
);
|
|
6303
|
+
} else {
|
|
6304
|
+
console.log("\nThis lead holds no dataset row \u2014 its values were combined into the lead it was merged with.");
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6076
6307
|
var integrationsCmd = program.command("integrations").description("Connect and inspect integrations");
|
|
6077
6308
|
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
6309
|
try {
|