@dench.com/cli 3.10.2 → 3.11.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/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 rowsFlag = getFlag(args, "--rows");
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":[]},
@@ -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",