@dench.com/cli 2.8.2-staging.112 → 2.8.3
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/crm.ts +29 -0
- package/dench.ts +77 -10
- package/leads.ts +529 -79
- package/lib/api-schemas.ts +163 -14
- package/lib/command-registry.ts +105 -2
- package/lib/enrichment-gateway.ts +6 -0
- package/lib/find-filter-options.generated.ts +206 -0
- package/package.json +1 -1
package/leads.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
searchJobs,
|
|
20
20
|
searchPeople,
|
|
21
21
|
} from "./lib/enrichment-gateway";
|
|
22
|
+
import { FIND_FILTER_OPTION_CATALOG } from "./lib/find-filter-options.generated";
|
|
22
23
|
|
|
23
24
|
class FindCliError extends Error {
|
|
24
25
|
constructor(message: string) {
|
|
@@ -39,13 +40,34 @@ const api = {
|
|
|
39
40
|
saveFind: makeFunctionReference<"action">(
|
|
40
41
|
"functions/workspaceArtifacts:saveFind",
|
|
41
42
|
),
|
|
43
|
+
updateFind: makeFunctionReference<"action">(
|
|
44
|
+
"functions/workspaceArtifacts:updateFind",
|
|
45
|
+
),
|
|
46
|
+
rename: makeFunctionReference<"mutation">(
|
|
47
|
+
"functions/workspaceArtifacts:rename",
|
|
48
|
+
),
|
|
49
|
+
remove: makeFunctionReference<"mutation">(
|
|
50
|
+
"functions/workspaceArtifacts:remove",
|
|
51
|
+
),
|
|
42
52
|
list: makeFunctionReference<"query">("functions/workspaceArtifacts:list"),
|
|
43
53
|
get: makeFunctionReference<"query">("functions/workspaceArtifacts:get"),
|
|
44
54
|
importArtifact: makeFunctionReference<"action">(
|
|
45
55
|
"functions/workspaceArtifacts:importArtifact",
|
|
46
56
|
),
|
|
57
|
+
resolveCompanyList: makeFunctionReference<"action">(
|
|
58
|
+
"functions/companyLists:resolveCompanyList",
|
|
59
|
+
),
|
|
47
60
|
};
|
|
48
61
|
|
|
62
|
+
/** createdFrom values that mark an artifact as a saved search / list. */
|
|
63
|
+
const LIST_CREATED_FROM = new Set([
|
|
64
|
+
"find_people",
|
|
65
|
+
"find_companies",
|
|
66
|
+
"find_jobs",
|
|
67
|
+
"mixed",
|
|
68
|
+
"watch_fire",
|
|
69
|
+
]);
|
|
70
|
+
|
|
49
71
|
function shift(args: string[], expected: string): string {
|
|
50
72
|
try {
|
|
51
73
|
return shiftRaw(args, expected);
|
|
@@ -64,50 +86,103 @@ function assertNoUnknownFlags(args: readonly string[], command: string): void {
|
|
|
64
86
|
}
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
|
|
89
|
+
type LeadsFindRange = { min?: number; max?: number };
|
|
90
|
+
|
|
91
|
+
type LeadsFindFlags = {
|
|
68
92
|
titles?: string[];
|
|
93
|
+
pastTitles?: string[];
|
|
94
|
+
personNames?: string[];
|
|
69
95
|
locations?: string[];
|
|
70
96
|
seniorities?: string[];
|
|
71
97
|
industries?: string[];
|
|
72
98
|
functions?: string[];
|
|
99
|
+
subFunctions?: string[];
|
|
73
100
|
companyDomain?: string;
|
|
101
|
+
companyDomains?: string[];
|
|
102
|
+
companyNames?: string[];
|
|
103
|
+
pastCompanyNames?: string[];
|
|
104
|
+
companyHeadquarters?: string[];
|
|
105
|
+
companyTypes?: string[];
|
|
106
|
+
universities?: string[];
|
|
107
|
+
skills?: string[];
|
|
108
|
+
languages?: string[];
|
|
109
|
+
headcount?: string[];
|
|
110
|
+
yearsInRole?: LeadsFindRange;
|
|
111
|
+
yearsAtCompany?: LeadsFindRange;
|
|
112
|
+
daysSinceJobChange?: LeadsFindRange;
|
|
113
|
+
foundedYears?: LeadsFindRange;
|
|
74
114
|
keywords?: string;
|
|
115
|
+
companyList?: string;
|
|
75
116
|
limit?: number;
|
|
76
117
|
pageToken?: string;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
118
|
+
name?: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** Parse `min:max` (either side optional; a bare number means min). */
|
|
122
|
+
function rangeFlag(args: string[], name: string): LeadsFindRange | undefined {
|
|
123
|
+
const raw = getFlag(args, name);
|
|
124
|
+
if (!raw) return undefined;
|
|
125
|
+
const [minRaw, maxRaw] = raw.includes(":") ? raw.split(":", 2) : [raw, ""];
|
|
126
|
+
const parsePart = (part: string, label: string): number | undefined => {
|
|
127
|
+
const trimmed = part.trim();
|
|
128
|
+
if (!trimmed) return undefined;
|
|
129
|
+
const parsed = Number(trimmed);
|
|
130
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
131
|
+
throw new FindCliError(
|
|
132
|
+
`${name} ${label} must be a non-negative integer (use min:max, e.g. 2:10)`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return parsed;
|
|
136
|
+
};
|
|
137
|
+
const min = parsePart(minRaw ?? "", "min");
|
|
138
|
+
const max = parsePart(maxRaw ?? "", "max");
|
|
139
|
+
if (min === undefined && max === undefined) {
|
|
140
|
+
throw new FindCliError(`${name} must be min:max, e.g. 2:10, 2: or :10`);
|
|
141
|
+
}
|
|
142
|
+
return { min, max };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseLeadsFindFlags(args: string[]): LeadsFindFlags {
|
|
146
|
+
const flags: LeadsFindFlags = {
|
|
147
|
+
titles: csvFlag(args, "--titles"),
|
|
148
|
+
pastTitles: csvFlag(args, "--past-titles"),
|
|
149
|
+
personNames: csvFlag(args, "--person-names"),
|
|
150
|
+
locations: csvFlag(args, "--locations"),
|
|
151
|
+
seniorities: csvFlag(args, "--seniorities"),
|
|
152
|
+
industries: csvFlag(args, "--industries"),
|
|
153
|
+
functions: csvFlag(args, "--functions"),
|
|
154
|
+
subFunctions: csvFlag(args, "--sub-functions"),
|
|
155
|
+
companyDomain: getFlag(args, "--company-domain"),
|
|
156
|
+
companyDomains: csvFlag(args, "--company-domains"),
|
|
157
|
+
companyNames: csvFlag(args, "--company-names"),
|
|
158
|
+
pastCompanyNames: csvFlag(args, "--past-company-names"),
|
|
159
|
+
companyHeadquarters: csvFlag(args, "--company-hq"),
|
|
160
|
+
companyTypes: csvFlag(args, "--company-types"),
|
|
161
|
+
universities: csvFlag(args, "--universities"),
|
|
162
|
+
skills: csvFlag(args, "--skills"),
|
|
163
|
+
languages: csvFlag(args, "--languages"),
|
|
164
|
+
headcount: csvFlag(args, "--headcount"),
|
|
165
|
+
yearsInRole: rangeFlag(args, "--years-in-role"),
|
|
166
|
+
yearsAtCompany: rangeFlag(args, "--years-at-company"),
|
|
167
|
+
daysSinceJobChange: rangeFlag(args, "--days-since-job-change"),
|
|
168
|
+
foundedYears: rangeFlag(args, "--founded-years"),
|
|
169
|
+
keywords: getFlag(args, "--keywords"),
|
|
170
|
+
companyList: getFlag(args, "--company-list"),
|
|
171
|
+
limit: parseLimit(args),
|
|
172
|
+
pageToken: getFlag(args, "--page-token"),
|
|
173
|
+
name: getFlag(args, "--save-as"),
|
|
174
|
+
};
|
|
87
175
|
assertNoUnknownFlags(args, "leads find");
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
!industries &&
|
|
94
|
-
!seniorities
|
|
95
|
-
) {
|
|
176
|
+
const hasFilter = Object.entries(flags).some(
|
|
177
|
+
([key, value]) =>
|
|
178
|
+
!["limit", "pageToken", "name"].includes(key) && value !== undefined,
|
|
179
|
+
);
|
|
180
|
+
if (!hasFilter) {
|
|
96
181
|
throw new FindCliError(
|
|
97
|
-
"dench leads find requires at least one of --titles, --locations, --company-domain, --keywords, --industries, --seniorities",
|
|
182
|
+
"dench leads find requires at least one of --titles, --locations, --company-domain, --keywords, --industries, --seniorities (or any other filter flag — run `dench leads help`)",
|
|
98
183
|
);
|
|
99
184
|
}
|
|
100
|
-
return
|
|
101
|
-
titles,
|
|
102
|
-
locations,
|
|
103
|
-
seniorities,
|
|
104
|
-
industries,
|
|
105
|
-
functions,
|
|
106
|
-
companyDomain,
|
|
107
|
-
keywords,
|
|
108
|
-
limit,
|
|
109
|
-
pageToken,
|
|
110
|
-
};
|
|
185
|
+
return flags;
|
|
111
186
|
}
|
|
112
187
|
|
|
113
188
|
export function assertLeadsFindArgs(args: string[]): void {
|
|
@@ -167,6 +242,85 @@ function asArray(value: unknown): unknown[] {
|
|
|
167
242
|
return Array.isArray(value) ? value : [];
|
|
168
243
|
}
|
|
169
244
|
|
|
245
|
+
/* ─── Company-list filter (--company-list) ─── */
|
|
246
|
+
|
|
247
|
+
type ResolvedCompanyList = {
|
|
248
|
+
source: "artifact" | "view";
|
|
249
|
+
title: string;
|
|
250
|
+
domains: string[];
|
|
251
|
+
names: string[];
|
|
252
|
+
totalCompanies: number;
|
|
253
|
+
truncated: boolean;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/** Convex document ids are long lowercase alphanumeric strings. */
|
|
257
|
+
const COMPANY_LIST_ID_RE = /^[a-z0-9]{25,40}$/;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `--company-list` accepts a saved-search artifact id (from
|
|
261
|
+
* `dench companies find` / `dench lists`) or a Company saved-view name.
|
|
262
|
+
* Id-shaped values are tried as artifacts first; anything else (or an
|
|
263
|
+
* id that doesn't resolve) is looked up as a view name.
|
|
264
|
+
*/
|
|
265
|
+
async function resolveCompanyListFlag(
|
|
266
|
+
ctx: FindCliContext,
|
|
267
|
+
value: string,
|
|
268
|
+
): Promise<ResolvedCompanyList> {
|
|
269
|
+
const trimmed = value.trim();
|
|
270
|
+
if (!trimmed) {
|
|
271
|
+
throw new FindCliError(
|
|
272
|
+
"--company-list requires a list artifact id or a Company view name",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (COMPANY_LIST_ID_RE.test(trimmed)) {
|
|
276
|
+
try {
|
|
277
|
+
return (await callAction(ctx, api.resolveCompanyList, {
|
|
278
|
+
artifactId: trimmed,
|
|
279
|
+
})) as ResolvedCompanyList;
|
|
280
|
+
} catch {
|
|
281
|
+
// Not an artifact (or not ours) — fall through to view-name lookup.
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
return (await callAction(ctx, api.resolveCompanyList, {
|
|
286
|
+
viewName: trimmed,
|
|
287
|
+
})) as ResolvedCompanyList;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
throw new FindCliError(
|
|
290
|
+
error instanceof Error ? error.message : String(error),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Saved-filter entry: the reference (re-resolved on hydrate) + title. */
|
|
296
|
+
function companyListFilterEntry(
|
|
297
|
+
ref: string,
|
|
298
|
+
resolved: ResolvedCompanyList,
|
|
299
|
+
): Record<string, unknown> {
|
|
300
|
+
return resolved.source === "artifact"
|
|
301
|
+
? { artifactId: ref.trim(), title: resolved.title }
|
|
302
|
+
: { viewName: ref.trim(), title: resolved.title };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Output block mirroring /api/workspace/find's `companyList` response. */
|
|
306
|
+
function companyListInfo(resolved: ResolvedCompanyList) {
|
|
307
|
+
return {
|
|
308
|
+
source: resolved.source,
|
|
309
|
+
title: resolved.title,
|
|
310
|
+
companiesUsed: resolved.domains.length + resolved.names.length,
|
|
311
|
+
totalCompanies: resolved.totalCompanies,
|
|
312
|
+
truncated: resolved.truncated,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function mergeUnique(
|
|
317
|
+
base: string[] | undefined,
|
|
318
|
+
extra: string[] | undefined,
|
|
319
|
+
): string[] | undefined {
|
|
320
|
+
const merged = [...new Set([...(base ?? []), ...(extra ?? [])])];
|
|
321
|
+
return merged.length > 0 ? merged : undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
170
324
|
function findTitle(
|
|
171
325
|
kind: "people" | "companies" | "jobs",
|
|
172
326
|
parts: string[],
|
|
@@ -197,22 +351,119 @@ export function leadsHelp(): void {
|
|
|
197
351
|
console.log(`Dench leads
|
|
198
352
|
|
|
199
353
|
Usage:
|
|
200
|
-
dench leads find --titles "VP Sales" --locations "San Francisco" [--company-domain acme.com] [--limit 500] [--page-token <token>] [--json]
|
|
354
|
+
dench leads find --titles "VP Sales" --locations "San Francisco" [--company-domain acme.com] [--save-as "Q3 targets"] [--limit 500] [--page-token <token>] [--json]
|
|
201
355
|
dench leads find --keywords "fintech" [--industries SaaS] [--seniorities director,vp] [--json]
|
|
356
|
+
dench leads find --company-list <artifactId|"Company view name"> --titles "VP Sales" [--json]
|
|
357
|
+
dench leads filters [--filter titles|locations|company-hq|universities|skills|keywords|seniorities|functions|sub-functions|industries|headcount|company-types|languages] [--search "query"] [--json]
|
|
358
|
+
|
|
359
|
+
Filter flags (all comma-separated lists unless noted):
|
|
360
|
+
People: --titles --past-titles --person-names --seniorities --functions
|
|
361
|
+
--sub-functions --languages --universities --skills --locations
|
|
362
|
+
Company: --company-names --past-company-names --company-domain (single)
|
|
363
|
+
--company-domains --company-hq --company-types --industries
|
|
364
|
+
--headcount (buckets like 11-50,51-200)
|
|
365
|
+
List: --company-list <artifactId|view name> — filter by a saved
|
|
366
|
+
company list (a \`dench companies find\` artifact or a Company
|
|
367
|
+
saved view). Resolves to the list's CURRENT companies on each
|
|
368
|
+
run; lists up to ~1000 companies are searched in one go.
|
|
369
|
+
Ranges: --years-in-role --years-at-company --days-since-job-change
|
|
370
|
+
--founded-years (min:max, e.g. 2:10, 2: or :10)
|
|
371
|
+
Query: --keywords "fintech, AI"
|
|
202
372
|
|
|
203
|
-
|
|
204
|
-
|
|
373
|
+
\`dench leads filters\` prints the accepted values: enum filters (seniorities,
|
|
374
|
+
functions, industries, headcount, company-types, languages) expect those
|
|
375
|
+
exact values; the rest are free text with curated suggestions.
|
|
376
|
+
|
|
377
|
+
Finds people through Dench search and writes a workspace list artifact.
|
|
378
|
+
Pass --save-as to name the list (otherwise a title is generated).
|
|
379
|
+
Manage lists with \`dench lists\`; import with \`dench artifacts import <id> --to lead\`.
|
|
380
|
+
Typical chain: dench companies find … --json → grab artifactId →
|
|
381
|
+
dench leads find --company-list <artifactId> --titles "VP Sales".
|
|
205
382
|
`);
|
|
206
383
|
}
|
|
207
384
|
|
|
385
|
+
/* ─── dench leads filters ─── */
|
|
386
|
+
|
|
387
|
+
/** kebab-case flag spellings accepted by `--filter` alongside catalog keys. */
|
|
388
|
+
const FILTER_KEY_ALIASES: Record<string, string> = {
|
|
389
|
+
"past-titles": "pastTitles",
|
|
390
|
+
"company-hq": "companyHeadquarters",
|
|
391
|
+
"company-types": "companyTypes",
|
|
392
|
+
"sub-functions": "subFunctions",
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Print the Lead Finder filter-option catalogs (offline; bundled data).
|
|
397
|
+
* Enum filters list exact gateway vocabularies; suggestion filters list
|
|
398
|
+
* curated starting points — custom free text always works for those.
|
|
399
|
+
*/
|
|
400
|
+
export function runFindFiltersCommand(opts: {
|
|
401
|
+
args: string[];
|
|
402
|
+
jsonOutput?: boolean;
|
|
403
|
+
}): void {
|
|
404
|
+
const args = [...opts.args];
|
|
405
|
+
const jsonOutput = opts.jsonOutput === true || hasFlag(args, "--json");
|
|
406
|
+
const filterRaw = getFlag(args, "--filter");
|
|
407
|
+
const search = getFlag(args, "--search")?.trim().toLowerCase();
|
|
408
|
+
assertNoUnknownFlags(args, "leads filters");
|
|
409
|
+
|
|
410
|
+
const resolveKey = (raw: string): string => {
|
|
411
|
+
const aliased = FILTER_KEY_ALIASES[raw.toLowerCase()];
|
|
412
|
+
if (aliased) return aliased;
|
|
413
|
+
const match = Object.keys(FIND_FILTER_OPTION_CATALOG).find(
|
|
414
|
+
(key) => key.toLowerCase() === raw.toLowerCase(),
|
|
415
|
+
);
|
|
416
|
+
if (match) return match;
|
|
417
|
+
throw new FindCliError(
|
|
418
|
+
`Unknown filter: ${raw}. Valid filters: ${[
|
|
419
|
+
...Object.keys(FIND_FILTER_OPTION_CATALOG),
|
|
420
|
+
...Object.keys(FILTER_KEY_ALIASES),
|
|
421
|
+
].join(", ")}`,
|
|
422
|
+
);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const keys = filterRaw
|
|
426
|
+
? [resolveKey(filterRaw)]
|
|
427
|
+
: Object.keys(FIND_FILTER_OPTION_CATALOG);
|
|
428
|
+
const filters: Record<string, unknown> = {};
|
|
429
|
+
for (const key of keys) {
|
|
430
|
+
const entry = FIND_FILTER_OPTION_CATALOG[key];
|
|
431
|
+
const groups = search
|
|
432
|
+
? entry.groups
|
|
433
|
+
.map((group) => ({
|
|
434
|
+
group: group.group,
|
|
435
|
+
values: group.values.filter((value) =>
|
|
436
|
+
value.toLowerCase().includes(search),
|
|
437
|
+
),
|
|
438
|
+
}))
|
|
439
|
+
.filter((group) => group.values.length > 0)
|
|
440
|
+
: entry.groups;
|
|
441
|
+
filters[key] = { label: entry.label, kind: entry.kind, groups };
|
|
442
|
+
}
|
|
443
|
+
console.log(
|
|
444
|
+
JSON.stringify(
|
|
445
|
+
{
|
|
446
|
+
note: "kind=enum values are exact gateway vocabularies; kind=suggestion values are curated starting points and custom free text is accepted.",
|
|
447
|
+
filters,
|
|
448
|
+
},
|
|
449
|
+
null,
|
|
450
|
+
jsonOutput ? 2 : 0,
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
208
455
|
export function companiesHelp(): void {
|
|
209
456
|
console.log(`Dench companies
|
|
210
457
|
|
|
211
458
|
Usage:
|
|
212
|
-
dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--industries SaaS] [--locations "San Francisco"] [--linkedin https://linkedin.com/company/acme] [--limit 500] [--page-token <token>] [--json]
|
|
459
|
+
dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--industries SaaS] [--locations "San Francisco"] [--linkedin https://linkedin.com/company/acme] [--save-as "Target accounts"] [--limit 500] [--page-token <token>] [--json]
|
|
213
460
|
|
|
214
|
-
Finds companies through Dench search and writes a workspace artifact.
|
|
215
|
-
|
|
461
|
+
Finds companies through Dench search and writes a workspace list artifact.
|
|
462
|
+
Pass --save-as to name the list (otherwise a title is generated).
|
|
463
|
+
Manage lists with \`dench lists\`; import with \`dench artifacts import <id> --to company\`.
|
|
464
|
+
The artifact id doubles as a company list: pass it to
|
|
465
|
+
\`dench leads find --company-list <id>\` or \`dench jobs find --company-list <id>\`
|
|
466
|
+
to search people or job postings at exactly these companies.
|
|
216
467
|
`);
|
|
217
468
|
}
|
|
218
469
|
|
|
@@ -220,10 +471,17 @@ export function jobsHelp(): void {
|
|
|
220
471
|
console.log(`Dench jobs
|
|
221
472
|
|
|
222
473
|
Usage:
|
|
223
|
-
dench jobs find --titles "Account Executive" --locations "San Francisco" [--company-domain acme.com] [--keywords remote] [--limit 500] [--page-token <token>] [--json]
|
|
474
|
+
dench jobs find --titles "Account Executive" --locations "San Francisco" [--company-domain acme.com] [--keywords remote] [--save-as "AE openings"] [--limit 500] [--page-token <token>] [--json]
|
|
475
|
+
dench jobs find --company-list <artifactId|"Company view name"> [--titles "Account Executive"] [--posted-after 2026-08-01] [--json]
|
|
224
476
|
|
|
225
|
-
|
|
226
|
-
|
|
477
|
+
Company filters: --company (name), --company-domain (single),
|
|
478
|
+
--company-domains (comma-separated), or --company-list (a saved company
|
|
479
|
+
list: a \`dench companies find\` artifact id or a Company saved-view name —
|
|
480
|
+
resolved to its current companies on each run).
|
|
481
|
+
|
|
482
|
+
Finds job postings through Dench search and writes a workspace list artifact.
|
|
483
|
+
Pass --save-as to name the list (otherwise a title is generated).
|
|
484
|
+
Manage lists with \`dench lists\`; import with \`dench artifacts import <id> --to job_posting\`.
|
|
227
485
|
`);
|
|
228
486
|
}
|
|
229
487
|
|
|
@@ -238,9 +496,138 @@ Usage:
|
|
|
238
496
|
|
|
239
497
|
\`list\` / \`show\` / \`import\` operate on find results (lead lists and job
|
|
240
498
|
searches). Bare \`dench artifacts\` still lists agent suggested-work artifacts.
|
|
499
|
+
Rename/delete saved searches with \`dench lists rename|delete\`.
|
|
241
500
|
`);
|
|
242
501
|
}
|
|
243
502
|
|
|
503
|
+
export function listsHelp(): void {
|
|
504
|
+
console.log(`Dench lists
|
|
505
|
+
|
|
506
|
+
Usage:
|
|
507
|
+
dench lists [--limit 25] [--json]
|
|
508
|
+
dench lists show <id> [--json]
|
|
509
|
+
dench lists rename <id> "New name" [--json]
|
|
510
|
+
dench lists update <id> [--title "New name"] [--filters-json '{"titles":["CTO"]}'] [--json]
|
|
511
|
+
dench lists delete <id> [--json]
|
|
512
|
+
|
|
513
|
+
Lists are saved searches from Find (people / companies / job postings).
|
|
514
|
+
Create one with \`dench leads|companies|jobs find --save-as "Name"\`.
|
|
515
|
+
Delete removes the list, its exported CSV/JSONL files, and any linked watch.
|
|
516
|
+
CRM lists (saved views on Leads/Companies/Job Postings) are managed with
|
|
517
|
+
\`dench crm views list|get|create|rename|delete\`.
|
|
518
|
+
`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* `dench lists …` — full management of saved searches. Returns false
|
|
523
|
+
* only for `help` so the caller can print usage.
|
|
524
|
+
*/
|
|
525
|
+
export async function runListsCommand(opts: {
|
|
526
|
+
convex: ConvexHttpClient;
|
|
527
|
+
args: string[];
|
|
528
|
+
jsonOutput?: boolean;
|
|
529
|
+
sessionToken?: string;
|
|
530
|
+
}): Promise<boolean> {
|
|
531
|
+
const args = [...opts.args];
|
|
532
|
+
const ctx: FindCliContext = {
|
|
533
|
+
convex: opts.convex,
|
|
534
|
+
args,
|
|
535
|
+
jsonOutput: opts.jsonOutput === true || hasFlag(args, "--json"),
|
|
536
|
+
sessionToken: opts.sessionToken,
|
|
537
|
+
};
|
|
538
|
+
const verb = args[0];
|
|
539
|
+
if (verb === "help" || verb === "--help") return false;
|
|
540
|
+
if (verb === undefined || verb === "list" || verb?.startsWith("--")) {
|
|
541
|
+
if (verb === "list") args.shift();
|
|
542
|
+
const limitRaw = getFlag(args, "--limit");
|
|
543
|
+
const limit = limitRaw ? Number(limitRaw) : 25;
|
|
544
|
+
assertNoUnknownFlags(args, "lists");
|
|
545
|
+
const rows = (await callQuery(ctx, api.list, {
|
|
546
|
+
kind: "table",
|
|
547
|
+
// Over-fetch: the query filters by kind only; saved searches are
|
|
548
|
+
// the table artifacts whose createdFrom is a find/watch source.
|
|
549
|
+
limit: Math.min(Math.max(limit * 2, limit), 200),
|
|
550
|
+
})) as Array<Record<string, unknown>>;
|
|
551
|
+
const lists = rows
|
|
552
|
+
.filter((row) => LIST_CREATED_FROM.has(String(row.createdFrom)))
|
|
553
|
+
.slice(0, limit)
|
|
554
|
+
.map((row) => ({
|
|
555
|
+
_id: row._id,
|
|
556
|
+
title: row.title,
|
|
557
|
+
createdFrom: row.createdFrom,
|
|
558
|
+
rowCount: row.rowCount,
|
|
559
|
+
createdAt: row.createdAt,
|
|
560
|
+
watchId: row.watchId,
|
|
561
|
+
}));
|
|
562
|
+
out(ctx, lists);
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
args.shift();
|
|
566
|
+
if (verb === "show") {
|
|
567
|
+
const artifactId = shift(args, "list id");
|
|
568
|
+
assertNoUnknownFlags(args, "lists show");
|
|
569
|
+
out(ctx, await callQuery(ctx, api.get, { artifactId }));
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
if (verb === "rename") {
|
|
573
|
+
const artifactId = shift(args, "list id");
|
|
574
|
+
const title = getFlag(args, "--title") ?? shift(args, "new name");
|
|
575
|
+
assertNoUnknownFlags(args, "lists rename");
|
|
576
|
+
out(
|
|
577
|
+
ctx,
|
|
578
|
+
await ctx.convex.mutation(api.rename, {
|
|
579
|
+
artifactId,
|
|
580
|
+
title,
|
|
581
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
582
|
+
} as never),
|
|
583
|
+
);
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
if (verb === "update") {
|
|
587
|
+
const artifactId = shift(args, "list id");
|
|
588
|
+
const title = getFlag(args, "--title");
|
|
589
|
+
const filtersJson = getFlag(args, "--filters-json");
|
|
590
|
+
assertNoUnknownFlags(args, "lists update");
|
|
591
|
+
let filters: unknown;
|
|
592
|
+
if (filtersJson !== undefined) {
|
|
593
|
+
try {
|
|
594
|
+
filters = JSON.parse(filtersJson);
|
|
595
|
+
} catch {
|
|
596
|
+
throw new FindCliError("--filters-json must be valid JSON");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (title === undefined && filters === undefined) {
|
|
600
|
+
throw new FindCliError(
|
|
601
|
+
"dench lists update requires --title and/or --filters-json",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
out(
|
|
605
|
+
ctx,
|
|
606
|
+
await callAction(ctx, api.updateFind, {
|
|
607
|
+
artifactId,
|
|
608
|
+
...(title !== undefined ? { title } : {}),
|
|
609
|
+
...(filters !== undefined ? { filters } : {}),
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
if (verb === "delete") {
|
|
615
|
+
const artifactId = shift(args, "list id");
|
|
616
|
+
assertNoUnknownFlags(args, "lists delete");
|
|
617
|
+
out(
|
|
618
|
+
ctx,
|
|
619
|
+
await ctx.convex.mutation(api.remove, {
|
|
620
|
+
artifactId,
|
|
621
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
622
|
+
} as never),
|
|
623
|
+
);
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
throw new FindCliError(
|
|
627
|
+
`Unknown lists verb: ${verb}. Try list, show, rename, update, delete.`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
244
631
|
export function assertJobsFindArgs(args: string[]): void {
|
|
245
632
|
const copy = [...args];
|
|
246
633
|
hasFlag(copy, "--json");
|
|
@@ -249,11 +636,13 @@ export function assertJobsFindArgs(args: string[]): void {
|
|
|
249
636
|
!copy.includes("--titles") &&
|
|
250
637
|
!copy.includes("--locations") &&
|
|
251
638
|
!copy.includes("--company-domain") &&
|
|
639
|
+
!copy.includes("--company-domains") &&
|
|
252
640
|
!copy.includes("--company") &&
|
|
641
|
+
!copy.includes("--company-list") &&
|
|
253
642
|
!copy.includes("--keywords")
|
|
254
643
|
) {
|
|
255
644
|
throw new FindCliError(
|
|
256
|
-
"dench jobs find requires at least one of --titles, --locations, --company-domain, --company, --keywords",
|
|
645
|
+
"dench jobs find requires at least one of --titles, --locations, --company-domain, --company-domains, --company, --company-list, --keywords",
|
|
257
646
|
);
|
|
258
647
|
}
|
|
259
648
|
}
|
|
@@ -298,52 +687,79 @@ export async function runLeadsCommand(opts: {
|
|
|
298
687
|
leadsHelp();
|
|
299
688
|
return;
|
|
300
689
|
}
|
|
690
|
+
if (verb === "filters") {
|
|
691
|
+
runFindFiltersCommand({ args, jsonOutput: ctx.jsonOutput });
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
301
694
|
if (verb !== "find") {
|
|
302
695
|
throw new FindCliError(`Unknown leads verb: ${verb}`);
|
|
303
696
|
}
|
|
304
|
-
const {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
keywords,
|
|
312
|
-
limit,
|
|
313
|
-
pageToken,
|
|
314
|
-
} = parseLeadsFindFlags(args);
|
|
697
|
+
const { name: listName, ...flags } = parseLeadsFindFlags(args);
|
|
698
|
+
// Resolve --company-list fresh per search so the filter tracks the
|
|
699
|
+
// list's CURRENT companies (domains exact, name fallback for rows
|
|
700
|
+
// without a domain).
|
|
701
|
+
const companyList = flags.companyList
|
|
702
|
+
? await resolveCompanyListFlag(ctx, flags.companyList)
|
|
703
|
+
: undefined;
|
|
315
704
|
try {
|
|
316
705
|
const result = await searchPeople(
|
|
317
706
|
{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
707
|
+
titles: flags.titles,
|
|
708
|
+
pastTitles: flags.pastTitles,
|
|
709
|
+
personNames: flags.personNames,
|
|
710
|
+
locations: flags.locations,
|
|
711
|
+
seniorities: flags.seniorities,
|
|
712
|
+
jobFunctions: flags.functions,
|
|
713
|
+
subFunctions: flags.subFunctions,
|
|
714
|
+
industries: flags.industries,
|
|
715
|
+
organizationDomains: flags.companyDomain
|
|
716
|
+
? [flags.companyDomain]
|
|
717
|
+
: undefined,
|
|
718
|
+
companyDomains: mergeUnique(flags.companyDomains, companyList?.domains),
|
|
719
|
+
companyNames: mergeUnique(flags.companyNames, companyList?.names),
|
|
720
|
+
pastCompanyNames: flags.pastCompanyNames,
|
|
721
|
+
companyHeadquarters: flags.companyHeadquarters,
|
|
722
|
+
companyTypes: flags.companyTypes,
|
|
723
|
+
universities: flags.universities,
|
|
724
|
+
skills: flags.skills,
|
|
725
|
+
languages: flags.languages,
|
|
726
|
+
headcountRanges: flags.headcount,
|
|
727
|
+
yearsInRole: flags.yearsInRole,
|
|
728
|
+
yearsAtCompany: flags.yearsAtCompany,
|
|
729
|
+
daysSinceJobChange: flags.daysSinceJobChange,
|
|
730
|
+
foundedYears: flags.foundedYears,
|
|
731
|
+
keywords: flags.keywords,
|
|
732
|
+
limit: flags.limit,
|
|
733
|
+
pageToken: flags.pageToken,
|
|
327
734
|
},
|
|
328
735
|
ctx.enrichmentGateway,
|
|
329
736
|
);
|
|
330
737
|
const people = asArray(result.people);
|
|
738
|
+
// Persist every set filter so the saved list hydrates the full rail.
|
|
739
|
+
const savedFilters: Record<string, unknown> = {};
|
|
740
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
741
|
+
if (key === "pageToken" || value === undefined) continue;
|
|
742
|
+
savedFilters[key] = value;
|
|
743
|
+
}
|
|
744
|
+
if (flags.companyList && companyList) {
|
|
745
|
+
// Store the reference (not the resolved domains) so re-running the
|
|
746
|
+
// saved search picks up the list's contents at that time.
|
|
747
|
+
savedFilters.companyList = companyListFilterEntry(
|
|
748
|
+
flags.companyList,
|
|
749
|
+
companyList,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
331
752
|
const artifact = await persist(ctx, {
|
|
332
753
|
createdFrom: "find_people",
|
|
333
|
-
title:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
industries,
|
|
343
|
-
companyDomain,
|
|
344
|
-
keywords,
|
|
345
|
-
limit,
|
|
346
|
-
},
|
|
754
|
+
title:
|
|
755
|
+
listName ??
|
|
756
|
+
findTitle("people", [
|
|
757
|
+
flags.titles?.[0] ?? "",
|
|
758
|
+
flags.keywords ?? "",
|
|
759
|
+
flags.companyDomain ?? "",
|
|
760
|
+
companyList?.title ?? "",
|
|
761
|
+
]),
|
|
762
|
+
filters: savedFilters,
|
|
347
763
|
people,
|
|
348
764
|
});
|
|
349
765
|
// metadata carries total / nextPageToken — pass the token back via
|
|
@@ -351,6 +767,7 @@ export async function runLeadsCommand(opts: {
|
|
|
351
767
|
out(ctx, {
|
|
352
768
|
...(artifact as Record<string, unknown>),
|
|
353
769
|
metadata: result.metadata,
|
|
770
|
+
...(companyList ? { companyList: companyListInfo(companyList) } : {}),
|
|
354
771
|
});
|
|
355
772
|
} catch (error) {
|
|
356
773
|
throw new FindCliError(
|
|
@@ -388,24 +805,40 @@ export async function runJobsCommand(opts: {
|
|
|
388
805
|
const titles = csvFlag(args, "--titles");
|
|
389
806
|
const locations = csvFlag(args, "--locations");
|
|
390
807
|
const companyDomain = getFlag(args, "--company-domain");
|
|
808
|
+
const companyDomains = csvFlag(args, "--company-domains");
|
|
391
809
|
const companyName = getFlag(args, "--company");
|
|
392
810
|
const keywords = getFlag(args, "--keywords");
|
|
393
811
|
const postedAfter = getFlag(args, "--posted-after");
|
|
812
|
+
const companyListRef = getFlag(args, "--company-list");
|
|
394
813
|
const limit = parseLimit(args);
|
|
395
814
|
const pageToken = getFlag(args, "--page-token");
|
|
815
|
+
const listName = getFlag(args, "--save-as");
|
|
396
816
|
assertNoUnknownFlags(args, "jobs find");
|
|
397
|
-
if (
|
|
817
|
+
if (
|
|
818
|
+
!titles &&
|
|
819
|
+
!locations &&
|
|
820
|
+
!companyDomain &&
|
|
821
|
+
!companyDomains &&
|
|
822
|
+
!companyName &&
|
|
823
|
+
!keywords &&
|
|
824
|
+
!companyListRef
|
|
825
|
+
) {
|
|
398
826
|
throw new FindCliError(
|
|
399
|
-
"dench jobs find requires at least one of --titles, --locations, --company-domain, --company, --keywords",
|
|
827
|
+
"dench jobs find requires at least one of --titles, --locations, --company-domain, --company-domains, --company, --company-list, --keywords",
|
|
400
828
|
);
|
|
401
829
|
}
|
|
830
|
+
const companyList = companyListRef
|
|
831
|
+
? await resolveCompanyListFlag(ctx, companyListRef)
|
|
832
|
+
: undefined;
|
|
402
833
|
try {
|
|
403
834
|
const result = await searchJobs(
|
|
404
835
|
{
|
|
405
836
|
titles,
|
|
406
837
|
locations,
|
|
407
838
|
companyDomain,
|
|
839
|
+
companyDomains: mergeUnique(companyDomains, companyList?.domains),
|
|
408
840
|
companyName,
|
|
841
|
+
companyNames: companyList?.names.length ? companyList.names : undefined,
|
|
409
842
|
keywords,
|
|
410
843
|
postedAfter,
|
|
411
844
|
limit,
|
|
@@ -416,21 +849,35 @@ export async function runJobsCommand(opts: {
|
|
|
416
849
|
const jobs = asArray(result.jobs);
|
|
417
850
|
const artifact = await persist(ctx, {
|
|
418
851
|
createdFrom: "find_jobs",
|
|
419
|
-
title:
|
|
852
|
+
title:
|
|
853
|
+
listName ??
|
|
854
|
+
findTitle("jobs", [
|
|
855
|
+
titles?.[0] ?? "",
|
|
856
|
+
companyName ?? "",
|
|
857
|
+
keywords ?? "",
|
|
858
|
+
companyList?.title ?? "",
|
|
859
|
+
]),
|
|
420
860
|
filters: {
|
|
421
861
|
titles,
|
|
422
862
|
locations,
|
|
423
863
|
companyDomain,
|
|
864
|
+
companyDomains,
|
|
424
865
|
companyName,
|
|
425
866
|
keywords,
|
|
426
867
|
postedAfter,
|
|
427
868
|
limit,
|
|
869
|
+
...(companyListRef && companyList
|
|
870
|
+
? {
|
|
871
|
+
companyList: companyListFilterEntry(companyListRef, companyList),
|
|
872
|
+
}
|
|
873
|
+
: {}),
|
|
428
874
|
},
|
|
429
875
|
jobs,
|
|
430
876
|
});
|
|
431
877
|
out(ctx, {
|
|
432
878
|
...(artifact as Record<string, unknown>),
|
|
433
879
|
metadata: result.metadata,
|
|
880
|
+
...(companyList ? { companyList: companyListInfo(companyList) } : {}),
|
|
434
881
|
});
|
|
435
882
|
} catch (error) {
|
|
436
883
|
throw new FindCliError(
|
|
@@ -474,6 +921,7 @@ export async function runCompaniesCommand(opts: {
|
|
|
474
921
|
const headcountRanges = csvFlag(args, "--headcount");
|
|
475
922
|
const limit = parseLimit(args);
|
|
476
923
|
const pageToken = getFlag(args, "--page-token");
|
|
924
|
+
const listName = getFlag(args, "--save-as");
|
|
477
925
|
assertNoUnknownFlags(args, "companies find");
|
|
478
926
|
if (
|
|
479
927
|
!domain &&
|
|
@@ -505,7 +953,9 @@ export async function runCompaniesCommand(opts: {
|
|
|
505
953
|
);
|
|
506
954
|
const artifact = await persist(ctx, {
|
|
507
955
|
createdFrom: "find_companies",
|
|
508
|
-
title:
|
|
956
|
+
title:
|
|
957
|
+
listName ??
|
|
958
|
+
findTitle("companies", [name ?? "", domain ?? "", keywords ?? ""]),
|
|
509
959
|
filters: {
|
|
510
960
|
domain,
|
|
511
961
|
name,
|