@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/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
- function parseLeadsFindFlags(args: string[]): {
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
- const titles = csvFlag(args, "--titles");
79
- const locations = csvFlag(args, "--locations");
80
- const seniorities = csvFlag(args, "--seniorities");
81
- const industries = csvFlag(args, "--industries");
82
- const functions = csvFlag(args, "--functions");
83
- const companyDomain = getFlag(args, "--company-domain");
84
- const keywords = getFlag(args, "--keywords");
85
- const limit = parseLimit(args);
86
- const pageToken = getFlag(args, "--page-token");
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
- if (
89
- !titles &&
90
- !locations &&
91
- !companyDomain &&
92
- !keywords &&
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
- Finds people through Dench search and writes a workspace artifact.
204
- Import with \`dench artifacts import <id> --to lead\`.
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
- Import with \`dench artifacts import <id> --to lead\`.
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
- Finds job postings through Dench search and writes a workspace artifact.
226
- Import with \`dench artifacts import <id> --to job_posting\`.
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
- titles,
306
- locations,
307
- seniorities,
308
- industries,
309
- functions,
310
- companyDomain,
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
- personTitles: titles,
319
- personLocations: locations,
320
- personSeniorities: seniorities,
321
- personFunctions: functions,
322
- organizationDomains: companyDomain ? [companyDomain] : undefined,
323
- organizationIndustries: industries,
324
- keywords,
325
- limit,
326
- pageToken,
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: findTitle("people", [
334
- titles?.[0] ?? "",
335
- keywords ?? "",
336
- companyDomain ?? "",
337
- ]),
338
- filters: {
339
- titles,
340
- locations,
341
- seniorities,
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 (!titles && !locations && !companyDomain && !companyName && !keywords) {
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: findTitle("jobs", [titles?.[0] ?? "", companyName ?? "", keywords ?? ""]),
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: findTitle("companies", [name ?? "", domain ?? "", keywords ?? ""]),
956
+ title:
957
+ listName ??
958
+ findTitle("companies", [name ?? "", domain ?? "", keywords ?? ""]),
509
959
  filters: {
510
960
  domain,
511
961
  name,