@dench.com/cli 3.10.2-staging.428 → 3.11.0-staging.434
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/app-sdk.d.ts +10 -0
- package/leads.ts +173 -14
- package/lib/app-sdk-catalog.generated.ts +1 -0
- package/lib/command-registry.ts +19 -0
- package/lib/references.generated.json +6 -6
- package/package.json +1 -1
package/app-sdk.d.ts
CHANGED
|
@@ -751,6 +751,13 @@ export interface ApiRequests {
|
|
|
751
751
|
limit?: number;
|
|
752
752
|
enrichment?: "all" | "enriched" | "in_progress" | "no_contacts" | "failed" | "not_enriched";
|
|
753
753
|
};
|
|
754
|
+
/** Buy contact details for 1–100 saved profiles and save them to the list. Skips fields already found, looked up empty, or in flight. Requires a signed-in member or their personal agent. — `dench lists enrich` — POST /api/v1/lists/{artifactId}/enrich */
|
|
755
|
+
"lists.enrich": {
|
|
756
|
+
artifactId: string;
|
|
757
|
+
rows: Record<string, unknown>[];
|
|
758
|
+
/** email is work email. */
|
|
759
|
+
fields: Array<"email" | "phone" | "personal_email">;
|
|
760
|
+
};
|
|
754
761
|
/** Explicitly import selected saved profiles, optionally filtered by enrichment, with a configured Lead status. Reuses CRM records; never sends outreach or triggers conversion. — `dench artifacts import` — POST /api/v1/lists/{artifactId}/import */
|
|
755
762
|
"lists.import": {
|
|
756
763
|
artifactId: string;
|
|
@@ -4268,6 +4275,7 @@ export interface ApiResponses {
|
|
|
4268
4275
|
}>;
|
|
4269
4276
|
"lists.list": unknown;
|
|
4270
4277
|
"lists.rows": unknown;
|
|
4278
|
+
"lists.enrich": unknown;
|
|
4271
4279
|
"lists.import": unknown;
|
|
4272
4280
|
"lists.get": unknown;
|
|
4273
4281
|
"lists.save": {
|
|
@@ -9551,6 +9559,8 @@ export type DenchApiTree = {
|
|
|
9551
9559
|
lists: {
|
|
9552
9560
|
/** Delete a saved search / list, its exported CSV/JSONL files, and any linked watch. (`dench lists delete`) */
|
|
9553
9561
|
delete(args: Req<"lists.delete">): Promise<Res<"lists.delete">>;
|
|
9562
|
+
/** Buy contact details for 1–100 saved profiles and save them to the list. Skips fields already found, looked up empty, or in flight. Requires a signed-in member or their personal agent. (`dench lists enrich`) */
|
|
9563
|
+
enrich(args: Req<"lists.enrich">): Promise<Res<"lists.enrich">>;
|
|
9554
9564
|
/** Get one saved search / list by artifact id. (`dench lists show`) */
|
|
9555
9565
|
get(args: Req<"lists.get">): Promise<Res<"lists.get">>;
|
|
9556
9566
|
/** Explicitly import selected saved profiles, optionally filtered by enrichment, with a configured Lead status. Reuses CRM records; never sends outreach or triggers conversion. (`dench artifacts import`) */
|
package/leads.ts
CHANGED
|
@@ -61,6 +61,9 @@ const api = {
|
|
|
61
61
|
listRows: makeFunctionReference<"action">(
|
|
62
62
|
"functions/workspaceArtifacts:listRows",
|
|
63
63
|
),
|
|
64
|
+
enrichSelected: makeFunctionReference<"action">(
|
|
65
|
+
"functions/workspaceArtifacts:enrichSelected",
|
|
66
|
+
),
|
|
64
67
|
get: makeFunctionReference<"query">("functions/workspaceArtifacts:get"),
|
|
65
68
|
reviews: makeFunctionReference<"query">("functions/prospectReviews:list"),
|
|
66
69
|
assess: makeFunctionReference<"mutation">("functions/prospectReviews:assess"),
|
|
@@ -80,6 +83,74 @@ const ENRICHMENT_FILTERS = [
|
|
|
80
83
|
"failed",
|
|
81
84
|
"not_enriched",
|
|
82
85
|
];
|
|
86
|
+
function rowIndexesFlag(args: string[]): number[] | undefined {
|
|
87
|
+
const rowsFlag = getFlag(args, "--rows");
|
|
88
|
+
return rowsFlag === undefined
|
|
89
|
+
? undefined
|
|
90
|
+
: rowsFlag.split(",").map((value) => {
|
|
91
|
+
if (!/^\d+$/.test(value.trim()))
|
|
92
|
+
throw new FindCliError(
|
|
93
|
+
"--rows must be comma-separated original zero-based row indices",
|
|
94
|
+
);
|
|
95
|
+
const index = Number(value);
|
|
96
|
+
if (!Number.isSafeInteger(index))
|
|
97
|
+
throw new FindCliError("Invalid row index");
|
|
98
|
+
return index;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const CONTACT_FIELDS = ["email", "phone", "personal_email"] as const;
|
|
103
|
+
type ContactField = (typeof CONTACT_FIELDS)[number];
|
|
104
|
+
/** One enrichSelected call accepts at most this many profiles. */
|
|
105
|
+
const ENRICH_BATCH = 100;
|
|
106
|
+
const ENRICH_WAIT_POLL_MS = 10_000;
|
|
107
|
+
/** Stays under the sandbox's 300s default foreground bash budget. */
|
|
108
|
+
const ENRICH_WAIT_DEFAULT_SECONDS = 240;
|
|
109
|
+
|
|
110
|
+
type SavedRowEntry = { index: number; row: unknown };
|
|
111
|
+
|
|
112
|
+
/** Page a saved list by original index until isDone. */
|
|
113
|
+
async function savedListEntries(
|
|
114
|
+
ctx: FindCliContext,
|
|
115
|
+
artifactId: string,
|
|
116
|
+
enrichment: string,
|
|
117
|
+
): Promise<SavedRowEntry[]> {
|
|
118
|
+
const entries: SavedRowEntry[] = [];
|
|
119
|
+
let cursor: string | null | undefined;
|
|
120
|
+
do {
|
|
121
|
+
const page = (await callAction(ctx, api.listRows, {
|
|
122
|
+
artifactId,
|
|
123
|
+
limit: 100,
|
|
124
|
+
enrichment,
|
|
125
|
+
...(cursor ? { cursor } : {}),
|
|
126
|
+
})) as {
|
|
127
|
+
page: SavedRowEntry[];
|
|
128
|
+
isDone: boolean;
|
|
129
|
+
continueCursor: string | null;
|
|
130
|
+
};
|
|
131
|
+
entries.push(...page.page);
|
|
132
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
133
|
+
} while (cursor);
|
|
134
|
+
return entries;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function enrichmentCounts(
|
|
138
|
+
ctx: FindCliContext,
|
|
139
|
+
artifactId: string,
|
|
140
|
+
selected: ReadonlySet<number>,
|
|
141
|
+
) {
|
|
142
|
+
const counts: Record<string, number> = {};
|
|
143
|
+
for (const state of ["in_progress", "enriched", "no_contacts", "failed"]) {
|
|
144
|
+
const rows = await savedListEntries(ctx, artifactId, state);
|
|
145
|
+
counts[state] = rows.filter((entry) => selected.has(entry.index)).length;
|
|
146
|
+
}
|
|
147
|
+
return counts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sleep(ms: number) {
|
|
151
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
152
|
+
}
|
|
153
|
+
|
|
83
154
|
function enrichmentFlag(args: string[]): string | undefined {
|
|
84
155
|
const value = getFlag(args, "--enrichment");
|
|
85
156
|
if (value !== undefined && !ENRICHMENT_FILTERS.includes(value))
|
|
@@ -659,6 +730,7 @@ Usage:
|
|
|
659
730
|
dench lists save --title "Target accounts" --companies-file <companies.json> [--json]
|
|
660
731
|
dench lists save --title "Open roles" --jobs-file <jobs.json> [--json]
|
|
661
732
|
dench lists rows <id> [--enrichment all|enriched|in_progress|no_contacts|failed|not_enriched] [--limit 100] [--cursor <cursor>] [--json]
|
|
733
|
+
dench lists enrich <id> [--fields email,phone,personal_email] [--rows 0,4,9] [--wait [--timeout 240]] [--json]
|
|
662
734
|
dench lists rename <id> "New name" [--json]
|
|
663
735
|
dench lists update <id> [--title "New name"] [--filters-json '{"titles":["CTO"]}'] [--json]
|
|
664
736
|
dench lists delete <id> [--json]
|
|
@@ -668,6 +740,10 @@ People lists are not CRM Leads. Searching and saving do not reveal new contact d
|
|
|
668
740
|
there is no mandatory per-profile ICP decision. Import only on explicit request, with the
|
|
669
741
|
requested configured CRM status. Follow pagination until isDone, even across
|
|
670
742
|
empty filtered pages. --rows uses original zero-based indices from lists rows.
|
|
743
|
+
Enrich buys contact details (default: work email) for every row, or only --rows, and
|
|
744
|
+
saves them to the list. It costs credits, so run it only when the user asks for contacts.
|
|
745
|
+
Found, empty and in-flight lookups are never bought twice, so rerunning is safe. --wait
|
|
746
|
+
polls until lookups finish (default 240s) and prints status counts.
|
|
671
747
|
dench lists reviews <id> [--limit 100] [--cursor <cursor>] [--json]
|
|
672
748
|
dench lists assess <id> --row-json '<profile>' --fit likely_fit|unlikely_fit|unknown --score 0-100 --reason '<short ICP reason>' [--json]
|
|
673
749
|
dench lists assess <id> --assessments-file <assessments.json> [--json]
|
|
@@ -927,6 +1003,102 @@ export async function runListsCommand(opts: {
|
|
|
927
1003
|
);
|
|
928
1004
|
return true;
|
|
929
1005
|
}
|
|
1006
|
+
if (verb === "enrich") {
|
|
1007
|
+
const artifactId = shift(args, "list id");
|
|
1008
|
+
const fieldsRaw = getFlag(args, "--fields") ?? "email";
|
|
1009
|
+
const fields = [
|
|
1010
|
+
...new Set(fieldsRaw.split(",").map((field) => field.trim())),
|
|
1011
|
+
];
|
|
1012
|
+
if (
|
|
1013
|
+
!fields.length ||
|
|
1014
|
+
fields.some((field) => !CONTACT_FIELDS.includes(field as ContactField))
|
|
1015
|
+
)
|
|
1016
|
+
throw new FindCliError(
|
|
1017
|
+
`--fields must be a comma-separated subset of ${CONTACT_FIELDS.join(",")}`,
|
|
1018
|
+
);
|
|
1019
|
+
const rowIndexes = rowIndexesFlag(args);
|
|
1020
|
+
const wait = hasFlag(args, "--wait");
|
|
1021
|
+
const timeoutRaw = getFlag(args, "--timeout");
|
|
1022
|
+
const timeoutSeconds =
|
|
1023
|
+
timeoutRaw === undefined
|
|
1024
|
+
? ENRICH_WAIT_DEFAULT_SECONDS
|
|
1025
|
+
: Number(timeoutRaw);
|
|
1026
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1)
|
|
1027
|
+
throw new FindCliError("--timeout must be a positive number of seconds");
|
|
1028
|
+
if (timeoutRaw !== undefined && !wait)
|
|
1029
|
+
throw new FindCliError("--timeout only applies with --wait");
|
|
1030
|
+
assertNoUnknownFlags(args, "lists enrich");
|
|
1031
|
+
|
|
1032
|
+
// The server skips fields already found, looked up empty, or in flight,
|
|
1033
|
+
// so sending every selected row never buys the same contact twice.
|
|
1034
|
+
let entries = await savedListEntries(ctx, artifactId, "all");
|
|
1035
|
+
if (rowIndexes) {
|
|
1036
|
+
const byIndex = new Map(entries.map((entry) => [entry.index, entry]));
|
|
1037
|
+
const missing = rowIndexes.filter((index) => !byIndex.has(index));
|
|
1038
|
+
if (missing.length)
|
|
1039
|
+
throw new FindCliError(
|
|
1040
|
+
`Row ${missing.join(", ")} is not in this list. Use original indices from dench lists rows.`,
|
|
1041
|
+
);
|
|
1042
|
+
entries = [...new Set(rowIndexes)].map(
|
|
1043
|
+
(index) => byIndex.get(index) as SavedRowEntry,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
if (!entries.length) throw new FindCliError("This list has no rows.");
|
|
1047
|
+
let queued = 0;
|
|
1048
|
+
let reused = 0;
|
|
1049
|
+
for (let start = 0; start < entries.length; start += ENRICH_BATCH) {
|
|
1050
|
+
const batch = entries.slice(start, start + ENRICH_BATCH);
|
|
1051
|
+
try {
|
|
1052
|
+
const result = (await callAction(ctx, api.enrichSelected, {
|
|
1053
|
+
artifactId,
|
|
1054
|
+
rows: batch.map((entry) => entry.row),
|
|
1055
|
+
fields,
|
|
1056
|
+
})) as { queued: number; reused: number };
|
|
1057
|
+
queued += result.queued;
|
|
1058
|
+
reused += result.reused;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
const message =
|
|
1061
|
+
error instanceof Error ? error.message : "Enrichment failed.";
|
|
1062
|
+
// The first batch never committed, so there is nothing to resume.
|
|
1063
|
+
if (start === 0) throw new FindCliError(message);
|
|
1064
|
+
throw new FindCliError(
|
|
1065
|
+
`Started ${queued} lookups for the first ${start} of ${entries.length} profiles. Rerun the same command to continue; finished and in-flight lookups are not bought again. ${message}`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const summary = {
|
|
1070
|
+
artifactId,
|
|
1071
|
+
fields,
|
|
1072
|
+
profiles: entries.length,
|
|
1073
|
+
queued,
|
|
1074
|
+
reused,
|
|
1075
|
+
};
|
|
1076
|
+
const readBack = `dench lists rows ${artifactId} --enrichment enriched --limit 100 --json`;
|
|
1077
|
+
if (!wait) {
|
|
1078
|
+
out(ctx, { ...summary, next: readBack });
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
1082
|
+
const selected = new Set(entries.map((entry) => entry.index));
|
|
1083
|
+
const inProgress = async () =>
|
|
1084
|
+
(await savedListEntries(ctx, artifactId, "in_progress")).filter((entry) =>
|
|
1085
|
+
selected.has(entry.index),
|
|
1086
|
+
).length;
|
|
1087
|
+
while ((await inProgress()) && Date.now() < deadline)
|
|
1088
|
+
await sleep(Math.min(ENRICH_WAIT_POLL_MS, deadline - Date.now()));
|
|
1089
|
+
const counts = await enrichmentCounts(ctx, artifactId, selected);
|
|
1090
|
+
out(ctx, {
|
|
1091
|
+
...summary,
|
|
1092
|
+
done: !counts.in_progress,
|
|
1093
|
+
counts,
|
|
1094
|
+
next: counts.in_progress
|
|
1095
|
+
? `Still running. Check again with: dench lists rows ${artifactId} --enrichment in_progress --json`
|
|
1096
|
+
: counts.failed
|
|
1097
|
+
? `dench lists rows ${artifactId} --enrichment failed --limit 100 --json`
|
|
1098
|
+
: readBack,
|
|
1099
|
+
});
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
930
1102
|
if (verb === "show") {
|
|
931
1103
|
const artifactId = shift(args, "list id");
|
|
932
1104
|
assertNoUnknownFlags(args, "lists show");
|
|
@@ -1439,20 +1611,7 @@ export async function runWorkspaceArtifactsCommand(opts: {
|
|
|
1439
1611
|
const pin = hasFlag(args, "--pin");
|
|
1440
1612
|
const status = getFlag(args, "--status");
|
|
1441
1613
|
const enrichment = enrichmentFlag(args);
|
|
1442
|
-
const
|
|
1443
|
-
const rowIndexes =
|
|
1444
|
-
rowsFlag === undefined
|
|
1445
|
-
? undefined
|
|
1446
|
-
: rowsFlag.split(",").map((value) => {
|
|
1447
|
-
if (!/^\d+$/.test(value.trim()))
|
|
1448
|
-
throw new FindCliError(
|
|
1449
|
-
"--rows must be comma-separated original zero-based row indices",
|
|
1450
|
-
);
|
|
1451
|
-
const index = Number(value);
|
|
1452
|
-
if (!Number.isSafeInteger(index))
|
|
1453
|
-
throw new FindCliError("Invalid row index");
|
|
1454
|
-
return index;
|
|
1455
|
-
});
|
|
1614
|
+
const rowIndexes = rowIndexesFlag(args);
|
|
1456
1615
|
assertNoUnknownFlags(args, "artifacts import");
|
|
1457
1616
|
out(
|
|
1458
1617
|
ctx,
|
|
@@ -140,6 +140,7 @@ export const APP_SDK_OPERATIONS: readonly AppSdkOperationInfo[] = [
|
|
|
140
140
|
{"id":"workspace.suggestedWork.list","group":"workspace","summary":"List draft suggested-work artifacts.","cli":"dench suggested-work","method":"GET","path":"/suggested-work","permission":"workspace:read","pathParams":[]},
|
|
141
141
|
{"id":"lists.list","group":"lists","summary":"List saved searches (find-result lists) in the workspace, newest first.","cli":"dench lists","method":"GET","path":"/lists","permission":"lists:read","pathParams":[]},
|
|
142
142
|
{"id":"lists.rows","group":"lists","summary":"Read saved rows with current enrichment results and original indices; cursor-paged, without repeating a search.","cli":"dench lists rows","method":"GET","path":"/lists/{artifactId}/rows","permission":"lists:read","pathParams":["artifactId"]},
|
|
143
|
+
{"id":"lists.enrich","group":"lists","summary":"Buy contact details for 1–100 saved profiles and save them to the list. Skips fields already found, looked up empty, or in flight. Requires a signed-in member or their personal agent.","cli":"dench lists enrich","method":"POST","path":"/lists/{artifactId}/enrich","permission":"lists:write","pathParams":["artifactId"]},
|
|
143
144
|
{"id":"lists.import","group":"lists","summary":"Explicitly import selected saved profiles, optionally filtered by enrichment, with a configured Lead status. Reuses CRM records; never sends outreach or triggers conversion.","cli":"dench artifacts import","method":"POST","path":"/lists/{artifactId}/import","permission":"lists:write","pathParams":["artifactId"]},
|
|
144
145
|
{"id":"lists.get","group":"lists","summary":"Get one saved search / list by artifact id.","cli":"dench lists show","method":"GET","path":"/lists/{artifactId}","permission":"lists:read","pathParams":["artifactId"]},
|
|
145
146
|
{"id":"lists.save","group":"lists","summary":"Save find results as a named list (rows + filters become a workspace artifact).","cli":"dench lists save","method":"POST","path":"/lists","permission":"lists:write","pathParams":[]},
|
package/lib/command-registry.ts
CHANGED
|
@@ -943,6 +943,25 @@ const rawApiOperations = [
|
|
|
943
943
|
responseSchema: anyResponse,
|
|
944
944
|
backend: convex("action", "functions/workspaceArtifacts:listRows"),
|
|
945
945
|
}),
|
|
946
|
+
op({
|
|
947
|
+
id: "lists.enrich",
|
|
948
|
+
group: "lists",
|
|
949
|
+
summary:
|
|
950
|
+
"Buy contact details for 1–100 saved profiles and save them to the list. Skips fields already found, looked up empty, or in flight. Requires a signed-in member or their personal agent.",
|
|
951
|
+
cli: "dench lists enrich",
|
|
952
|
+
method: "POST",
|
|
953
|
+
path: "/lists/{artifactId}/enrich",
|
|
954
|
+
requestSchema: z.object({
|
|
955
|
+
artifactId: z.string().min(1),
|
|
956
|
+
rows: z.array(z.record(z.string(), z.unknown())).min(1).max(100),
|
|
957
|
+
fields: z
|
|
958
|
+
.array(z.enum(["email", "phone", "personal_email"]))
|
|
959
|
+
.min(1)
|
|
960
|
+
.describe("email is work email."),
|
|
961
|
+
}),
|
|
962
|
+
responseSchema: anyResponse,
|
|
963
|
+
backend: convex("action", "functions/workspaceArtifacts:enrichSelected"),
|
|
964
|
+
}),
|
|
946
965
|
op({
|
|
947
966
|
id: "lists.import",
|
|
948
967
|
group: "lists",
|