@dench.com/cli 2.7.10-staging.100 → 2.8.0-staging.107

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 CHANGED
@@ -49,9 +49,6 @@ import {
49
49
  getRequiredFieldsForApolloPath,
50
50
  } from "./lib/crm-enrichment";
51
51
  import {
52
- type AviatoEnrichCompanyBody,
53
- type AviatoEnrichPersonBody,
54
- aviatoEnrichCompany,
55
52
  aviatoEnrichPerson,
56
53
  type EnrichCompanyBody,
57
54
  EnrichmentGatewayError,
@@ -61,14 +58,6 @@ import {
61
58
  enrichPerson,
62
59
  } from "./lib/enrichment-gateway";
63
60
 
64
- // Contact-info fields that Aviato does not return.
65
- const CONTACT_REQUIRED_FIELDS = ["phone", "email", "personal_email"] as const;
66
- type ContactRequiredField = (typeof CONTACT_REQUIRED_FIELDS)[number];
67
-
68
- function isContactField(field: string): field is ContactRequiredField {
69
- return (CONTACT_REQUIRED_FIELDS as readonly string[]).includes(field);
70
- }
71
-
72
61
  type JsonRecord = Record<string, unknown>;
73
62
 
74
63
  const CRM_BATCH_CHUNK_SIZE = 200;
@@ -2892,8 +2881,6 @@ type CrmEntryForEnrichment = {
2892
2881
  fields: Record<string, unknown>;
2893
2882
  };
2894
2883
 
2895
- type CliEnrichmentProvider = "fullenrich" | "aviato";
2896
-
2897
2884
  type EnrichmentTarget = {
2898
2885
  category: EnrichmentCategory;
2899
2886
  column: EnrichmentColumnDef;
@@ -2905,7 +2892,6 @@ type CellEnrichmentArgs = {
2905
2892
  objectName: string;
2906
2893
  entryId: string;
2907
2894
  fieldName: string;
2908
- provider?: CliEnrichmentProvider;
2909
2895
  category?: EnrichmentCategory;
2910
2896
  inputFieldName?: string;
2911
2897
  apolloPath?: string;
@@ -2925,16 +2911,6 @@ function parseCategoryFlag(
2925
2911
  throw new CrmCliError("--category must be people or company");
2926
2912
  }
2927
2913
 
2928
- function parseProviderFlag(
2929
- value: string | undefined,
2930
- ): CliEnrichmentProvider | undefined {
2931
- if (value === undefined) return undefined;
2932
- if (value === "fullenrich" || value === "aviato") return value;
2933
- throw new CrmCliError(
2934
- `--provider must be fullenrich or aviato; received "${value}"`,
2935
- );
2936
- }
2937
-
2938
2914
  function parsePositiveInt(
2939
2915
  flag: string,
2940
2916
  value: string | undefined,
@@ -3107,21 +3083,19 @@ async function resolveEnrichmentTarget(
3107
3083
  * chat-turn `enrich_person` / `enrich_company` / `enrich_person_basic` /
3108
3084
  * `enrich_company_basic` tools do — pass every structured identifier the
3109
3085
  * row has (email, firstName, lastName, linkedinUrl, domain, …) and let the
3110
- * shared gateway client (`enrichPerson`, `enrichCompany`,
3111
- * `aviatoEnrichPerson`, `aviatoEnrichCompany`) validate + dispatch.
3086
+ * shared gateway client (`enrichPerson`, `enrichCompany`) validate +
3087
+ * dispatch. The gateway owns the waterfall; the CLI never names a vendor.
3112
3088
  *
3113
3089
  * The previous implementation collapsed the entry to a single
3114
3090
  * `--input-field` value and called `enrichPersonByIdentifier`, which
3115
- * threw away the rest of the row and made FullEnrich fail with
3091
+ * threw away the rest of the row and made enrichment fail with
3116
3092
  * `missing_people_identifier` whenever the input field was just an
3117
3093
  * Email (no first/last name reached the gateway, so the validator
3118
- * rejected the request before any provider was called).
3094
+ * rejected the request before any lookup ran).
3119
3095
  */
3120
3096
  async function callEnrichmentGatewayForEntry(args: {
3121
3097
  target: EnrichmentTarget;
3122
3098
  entry: CrmEntryForEnrichment;
3123
- /** Explicit provider override. Defaults: people=fullenrich, company=aviato. */
3124
- provider?: CliEnrichmentProvider;
3125
3099
  gateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
3126
3100
  }): Promise<Record<string, unknown>> {
3127
3101
  const { entry, target } = args;
@@ -3134,43 +3108,27 @@ async function callEnrichmentGatewayForEntry(args: {
3134
3108
  );
3135
3109
  }
3136
3110
  const isCompany = target.category === "company";
3137
- const resolvedProvider =
3138
- args.provider ?? (isCompany ? "aviato" : "fullenrich");
3139
-
3140
- if (resolvedProvider === "aviato") {
3141
- // Aviato does not return phone or email — surface a clear error
3142
- // before we waste a gateway round-trip.
3143
- const contactFields = target.requiredFields.filter(isContactField);
3144
- if (contactFields.length > 0) {
3145
- throw new CrmCliError(
3146
- `Aviato does not return phone or email; use --provider fullenrich for contact fields (${contactFields.join(", ")}).`,
3147
- );
3148
- }
3149
- if (isCompany) {
3150
- return aviatoEnrichCompany(
3151
- buildAviatoCompanyBodyFromEntry(entry),
3152
- args.gateway,
3153
- );
3154
- }
3155
- return aviatoEnrichPerson(
3156
- buildAviatoPersonBodyFromEntry(entry),
3157
- args.gateway,
3158
- );
3159
- }
3160
-
3161
- // FullEnrich path. Mirrors `enrichPersonStep` / `enrichCompanyStep` in
3162
- // src/workflows/enrichment-steps.ts: build a structured body, hand it
3163
- // to the same shared gateway client.
3164
3111
  if (isCompany) {
3165
3112
  return enrichCompany(
3166
3113
  buildCompanyBodyFromEntry(entry, target.requiredFields),
3167
3114
  args.gateway,
3168
3115
  );
3169
3116
  }
3170
- return enrichPerson(
3171
- buildPersonBodyFromEntry(entry, target.requiredFields),
3172
- args.gateway,
3173
- );
3117
+ const personBody = buildPersonBodyFromEntry(entry, target.requiredFields);
3118
+ // Contact cells stay on /person/contact. A LinkedIn URL for a profile
3119
+ // field (name, title, headline) uses /person so the gateway waterfall
3120
+ // can fill graph data without a contact job.
3121
+ const isContactField = Boolean(target.column.enrichFields?.length);
3122
+ if (!isContactField && personBody.linkedinUrl) {
3123
+ return aviatoEnrichPerson(
3124
+ {
3125
+ linkedinUrl: personBody.linkedinUrl,
3126
+ email: personBody.email,
3127
+ },
3128
+ args.gateway,
3129
+ );
3130
+ }
3131
+ return enrichPerson(personBody, args.gateway);
3174
3132
  }
3175
3133
 
3176
3134
  // ── Body builders ────────────────────────────────────────────────────────
@@ -3275,24 +3233,6 @@ export function buildCompanyBodyFromEntry(
3275
3233
  };
3276
3234
  }
3277
3235
 
3278
- export function buildAviatoPersonBodyFromEntry(
3279
- entry: CrmEntryForEnrichment,
3280
- ): AviatoEnrichPersonBody {
3281
- return {
3282
- linkedinUrl: readEntryField(entry, PEOPLE_LINKEDIN_FIELDS),
3283
- email: readEntryField(entry, PEOPLE_EMAIL_FIELDS),
3284
- };
3285
- }
3286
-
3287
- export function buildAviatoCompanyBodyFromEntry(
3288
- entry: CrmEntryForEnrichment,
3289
- ): AviatoEnrichCompanyBody {
3290
- return {
3291
- website: readEntryField(entry, COMPANY_WEBSITE_FIELDS),
3292
- linkedinUrl: readEntryField(entry, COMPANY_LINKEDIN_FIELDS),
3293
- };
3294
- }
3295
-
3296
3236
  async function enrichOneEntry(
3297
3237
  ctx: CrmCliContext,
3298
3238
  args: Omit<CellEnrichmentArgs, "entryId"> & {
@@ -3305,7 +3245,6 @@ async function enrichOneEntry(
3305
3245
  const payload = await callEnrichmentGatewayForEntry({
3306
3246
  target: args.target,
3307
3247
  entry: args.entry,
3308
- provider: args.provider,
3309
3248
  gateway: ctx.enrichmentGateway,
3310
3249
  });
3311
3250
  value = extractEnrichmentValue(payload, args.target.column);
@@ -3341,7 +3280,7 @@ async function enrichOneEntry(
3341
3280
  entryId: args.entry.id,
3342
3281
  fieldName: args.fieldName,
3343
3282
  value,
3344
- provider: args.provider ?? "gateway",
3283
+ provider: "gateway",
3345
3284
  overwrite: args.overwrite,
3346
3285
  },
3347
3286
  )) as Record<string, unknown>;
@@ -3458,7 +3397,6 @@ async function runPeopleCommand(ctx: CrmCliContext): Promise<void> {
3458
3397
  }
3459
3398
  case "enrich": {
3460
3399
  const entryId = shift(ctx.args, "person entry id");
3461
- const provider = parseProviderFlag(getFlag(ctx.args, "--provider"));
3462
3400
  const fieldName = getFlag(ctx.args, "--field") ?? "LinkedIn URL";
3463
3401
  const inputFieldName = getFlag(ctx.args, "--input-field");
3464
3402
  const overwrite = hasFlag(ctx.args, "--overwrite");
@@ -3469,7 +3407,6 @@ async function runPeopleCommand(ctx: CrmCliContext): Promise<void> {
3469
3407
  objectName: "people",
3470
3408
  entryId,
3471
3409
  fieldName,
3472
- provider,
3473
3410
  category: "people",
3474
3411
  inputFieldName,
3475
3412
  overwrite,
@@ -3523,7 +3460,6 @@ async function runCompaniesCommand(ctx: CrmCliContext): Promise<void> {
3523
3460
  }
3524
3461
  case "enrich": {
3525
3462
  const entryId = shift(ctx.args, "company entry id");
3526
- const provider = parseProviderFlag(getFlag(ctx.args, "--provider"));
3527
3463
  const fieldName = getFlag(ctx.args, "--field") ?? "Company Name";
3528
3464
  const inputFieldName = getFlag(ctx.args, "--input-field") ?? "Domain";
3529
3465
  const overwrite = hasFlag(ctx.args, "--overwrite");
@@ -3534,7 +3470,6 @@ async function runCompaniesCommand(ctx: CrmCliContext): Promise<void> {
3534
3470
  objectName: "company",
3535
3471
  entryId,
3536
3472
  fieldName,
3537
- provider,
3538
3473
  category: "company",
3539
3474
  inputFieldName,
3540
3475
  overwrite,
@@ -3556,7 +3491,6 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
3556
3491
  const objectName = shift(ctx.args, "object name");
3557
3492
  const entryId = shift(ctx.args, "entry id");
3558
3493
  const fieldName = shift(ctx.args, "field name");
3559
- const provider = parseProviderFlag(getFlag(ctx.args, "--provider"));
3560
3494
  const inputFieldName = getFlag(ctx.args, "--input-field");
3561
3495
  const category = parseCategoryFlag(getFlag(ctx.args, "--category"));
3562
3496
  const apolloPath = getFlag(ctx.args, "--apollo-path");
@@ -3568,7 +3502,6 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
3568
3502
  objectName,
3569
3503
  entryId,
3570
3504
  fieldName,
3571
- provider,
3572
3505
  category,
3573
3506
  inputFieldName,
3574
3507
  apolloPath,
@@ -3581,7 +3514,6 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
3581
3514
  const objectName = shift(ctx.args, "object name");
3582
3515
  const fieldName = getFlag(ctx.args, "--field");
3583
3516
  const missingOnly = !hasFlag(ctx.args, "--all");
3584
- const provider = parseProviderFlag(getFlag(ctx.args, "--provider"));
3585
3517
  const inputFieldName = getFlag(ctx.args, "--input-field");
3586
3518
  const category = parseCategoryFlag(getFlag(ctx.args, "--category"));
3587
3519
  const apolloPath = getFlag(ctx.args, "--apollo-path");
@@ -3590,7 +3522,7 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
3590
3522
  assertNoUnknownFlags(ctx.args, "crm enrich object");
3591
3523
  if (!fieldName) {
3592
3524
  throw new CrmCliError(
3593
- "dench crm enrich object <name> --field <field> [--input-field <field>] [--category people|company] [--apollo-path <path>] [--limit N] [--provider ...] [--all] [--overwrite]",
3525
+ "dench crm enrich object <name> --field <field> [--input-field <field>] [--category people|company] [--apollo-path <path>] [--limit N] [--all] [--overwrite]",
3594
3526
  );
3595
3527
  }
3596
3528
  out(
@@ -3598,7 +3530,6 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
3598
3530
  await enrichObjectThroughGateway(ctx, {
3599
3531
  objectName,
3600
3532
  fieldName,
3601
- provider,
3602
3533
  category,
3603
3534
  inputFieldName,
3604
3535
  apolloPath,
package/dench.ts CHANGED
@@ -471,6 +471,12 @@ Usage:
471
471
  dench memory search "query" [--limit 8] [--json]
472
472
  dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--scope user|org] [--json]
473
473
  dench artifacts [--limit 25] [--json]
474
+ dench artifacts list [--limit 25] [--json]
475
+ dench artifacts show <id> [--json]
476
+ dench artifacts import <id> --to lead|company|job_posting [--view "Q3 outbound"] [--pin] [--json]
477
+ dench leads find --titles "VP Sales" [--locations "San Francisco"] [--limit 500] [--json]
478
+ dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--limit 500] [--json]
479
+ dench jobs find --titles "Account Executive" [--locations "San Francisco"] [--limit 500] [--json]
474
480
  dench suggested-work [--json]
475
481
  dench approval request "message" [--json]
476
482
  dench approval approve <approvalId> [--evidence "User said yes in chat"] [--json]
@@ -737,9 +743,37 @@ function artifactsHelp() {
737
743
 
738
744
  Usage:
739
745
  dench artifacts [--limit 25] [--json]
746
+ dench artifacts list [--limit 25] [--json]
747
+ dench artifacts show <id> [--json]
748
+ dench artifacts import <id> --to lead|company|job_posting [--view "Q3 outbound"] [--pin] [--json]
740
749
  dench suggested-work [--json]
741
750
 
742
- Artifacts are durable outputs for human or agent review.
751
+ \`list\` / \`show\` / \`import\` operate on find results. Bare \`dench artifacts\`
752
+ lists agent suggested-work artifacts.
753
+ `);
754
+ }
755
+
756
+ function leadsHelp() {
757
+ console.log(`Dench leads
758
+
759
+ Usage:
760
+ dench leads find --titles "VP Sales" --locations "San Francisco" [--company-domain acme.com] [--keywords fintech] [--limit 500] [--json]
761
+ `);
762
+ }
763
+
764
+ function companiesHelp() {
765
+ console.log(`Dench companies
766
+
767
+ Usage:
768
+ dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--industries SaaS] [--locations "San Francisco"] [--limit 500] [--json]
769
+ `);
770
+ }
771
+
772
+ function jobsHelp() {
773
+ console.log(`Dench jobs
774
+
775
+ Usage:
776
+ dench jobs find --titles "Account Executive" --locations "San Francisco" [--company-domain acme.com] [--keywords remote] [--limit 500] [--json]
743
777
  `);
744
778
  }
745
779
 
@@ -823,9 +857,12 @@ Lists every app connected to this organization, from both brokers:
823
857
  - Composio apps (Slack, GitHub, Notion, …), each with a
824
858
  \`connectedAccountId\` you can pass to \`dench tool run --account\`.
825
859
  - Gmail, Outlook, Calendar and LinkedIn, which Dench connects itself.
826
- These show \`source: "unipile"\` and have no connected-account id;
827
- reach their data through \`dench crm\` (calendar events sync into the
828
- \`meeting\` object) and \`dench email\`.
860
+ These show \`source: "unipile"\` and no connected-account id, but they
861
+ run through the SAME \`dench tool search\` / \`dench tool run\` — find
862
+ the slug and run it. Only \`dench tool connect\` is Composio-only; a
863
+ human connects these four from the workspace Marketplace.
864
+ For reading, the CRM is usually better: calendar events sync into
865
+ \`calendar_event\` and recordings into \`meeting\`.
829
866
 
830
867
  Composio does not broker those four, so they appear ONLY here — a
831
868
  missing calendar in this list means it is genuinely not connected.
@@ -2525,6 +2562,7 @@ function stripRuntimeFlags(args: string[]) {
2525
2562
  "--prod",
2526
2563
  "--staging",
2527
2564
  "--use-dev-env",
2565
+ "--json",
2528
2566
  ]);
2529
2567
  const valueFlags = new Set(["--convex-url", "--host", "--backend"]);
2530
2568
  const stripped: string[] = [];
@@ -2556,12 +2594,50 @@ function requireAuthenticatedRuntime(
2556
2594
  throw loginRequiredError(command);
2557
2595
  }
2558
2596
 
2597
+ async function resolveFindCommandAuth(command: string) {
2598
+ const runtime = await getRuntime();
2599
+ if (runtime.mode === "dev") {
2600
+ const gatewayAuth = await resolveGatewayCommandAuth(
2601
+ runtime,
2602
+ `${command} find`,
2603
+ );
2604
+ return {
2605
+ convex: runtime.client,
2606
+ sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
2607
+ enrichmentGateway: {
2608
+ apiKey: gatewayAuth.bearerToken,
2609
+ baseUrl: gatewayAuth.gatewayBaseUrl,
2610
+ },
2611
+ };
2612
+ }
2613
+ const authedRuntime = requireAuthenticatedRuntime(runtime, command);
2614
+ const gatewayAuth = await resolveGatewayCommandAuth(
2615
+ authedRuntime,
2616
+ `${command} find`,
2617
+ );
2618
+ return {
2619
+ convex: authedRuntime.client,
2620
+ sessionToken: authedRuntime.sessionToken,
2621
+ enrichmentGateway: {
2622
+ apiKey: gatewayAuth.bearerToken,
2623
+ baseUrl: gatewayAuth.gatewayBaseUrl,
2624
+ },
2625
+ };
2626
+ }
2627
+
2559
2628
  type GatewayCommandAuth = {
2560
2629
  bearerToken: string;
2561
2630
  gatewayBaseUrl?: string;
2562
2631
  gatewayHeaders?: Record<string, string>;
2563
2632
  };
2564
2633
 
2634
+ function envGatewayBaseUrlOverride(): string | undefined {
2635
+ const explicit =
2636
+ process.env.DENCH_GATEWAY_URL?.trim() || process.env.GATEWAY_URL?.trim();
2637
+ if (!explicit) return undefined;
2638
+ return explicit.replace(/\/+$/, "").replace(/\/v1$/, "");
2639
+ }
2640
+
2565
2641
  function gatewayBaseUrlForCli(value: unknown): string | undefined {
2566
2642
  if (typeof value !== "string") return undefined;
2567
2643
  const trimmed = value.trim().replace(/\/+$/, "");
@@ -2629,7 +2705,10 @@ async function resolveGatewayCommandAuth(
2629
2705
  command: string,
2630
2706
  ): Promise<GatewayCommandAuth> {
2631
2707
  if (runtime.mode === "apiKey") {
2632
- return { bearerToken: runtime.sessionToken };
2708
+ return {
2709
+ bearerToken: runtime.sessionToken,
2710
+ gatewayBaseUrl: envGatewayBaseUrlOverride(),
2711
+ };
2633
2712
  }
2634
2713
 
2635
2714
  if (runtime.mode === "session") {
@@ -2655,7 +2734,8 @@ async function resolveGatewayCommandAuth(
2655
2734
  }
2656
2735
  return {
2657
2736
  bearerToken: gatewayKey,
2658
- gatewayBaseUrl: gatewayBaseUrlForCli(body.gatewayBaseUrl),
2737
+ gatewayBaseUrl:
2738
+ envGatewayBaseUrlOverride() ?? gatewayBaseUrlForCli(body.gatewayBaseUrl),
2659
2739
  gatewayHeaders:
2660
2740
  body.gatewayHeaders &&
2661
2741
  typeof body.gatewayHeaders === "object" &&
@@ -2666,7 +2746,12 @@ async function resolveGatewayCommandAuth(
2666
2746
  }
2667
2747
 
2668
2748
  const envApiKey = process.env.DENCH_API_KEY?.trim();
2669
- if (envApiKey) return { bearerToken: envApiKey };
2749
+ if (envApiKey) {
2750
+ return {
2751
+ bearerToken: envApiKey,
2752
+ gatewayBaseUrl: envGatewayBaseUrlOverride(),
2753
+ };
2754
+ }
2670
2755
  throw loginRequiredError(command);
2671
2756
  }
2672
2757
 
@@ -4166,6 +4251,21 @@ async function main() {
4166
4251
  return;
4167
4252
  }
4168
4253
 
4254
+ if (command === "leads" && (subcommand === "help" || hasFlag("--help"))) {
4255
+ leadsHelp();
4256
+ return;
4257
+ }
4258
+
4259
+ if (command === "companies" && (subcommand === "help" || hasFlag("--help"))) {
4260
+ companiesHelp();
4261
+ return;
4262
+ }
4263
+
4264
+ if (command === "jobs" && (subcommand === "help" || hasFlag("--help"))) {
4265
+ jobsHelp();
4266
+ return;
4267
+ }
4268
+
4169
4269
  if (command === "status" && hasFlag("--help")) {
4170
4270
  statusHelp();
4171
4271
  return;
@@ -4246,6 +4346,53 @@ async function main() {
4246
4346
  return;
4247
4347
  }
4248
4348
 
4349
+ if (command === "leads") {
4350
+ const { runLeadsCommand, assertLeadsFindArgs } = await import("./leads");
4351
+ const leadArgs = stripRuntimeFlags(args.slice(args.indexOf("leads") + 1));
4352
+ assertLeadsFindArgs(leadArgs);
4353
+ const auth = await resolveFindCommandAuth("dench leads");
4354
+ await runLeadsCommand({
4355
+ convex: auth.convex,
4356
+ args: leadArgs,
4357
+ jsonOutput: json,
4358
+ sessionToken: auth.sessionToken,
4359
+ enrichmentGateway: auth.enrichmentGateway,
4360
+ });
4361
+ return;
4362
+ }
4363
+
4364
+ if (command === "companies") {
4365
+ const { runCompaniesCommand, assertCompaniesFindArgs } = await import("./leads");
4366
+ const companyArgs = stripRuntimeFlags(
4367
+ args.slice(args.indexOf("companies") + 1),
4368
+ );
4369
+ assertCompaniesFindArgs(companyArgs);
4370
+ const auth = await resolveFindCommandAuth("dench companies");
4371
+ await runCompaniesCommand({
4372
+ convex: auth.convex,
4373
+ args: companyArgs,
4374
+ jsonOutput: json,
4375
+ sessionToken: auth.sessionToken,
4376
+ enrichmentGateway: auth.enrichmentGateway,
4377
+ });
4378
+ return;
4379
+ }
4380
+
4381
+ if (command === "jobs") {
4382
+ const { runJobsCommand, assertJobsFindArgs } = await import("./leads");
4383
+ const jobArgs = stripRuntimeFlags(args.slice(args.indexOf("jobs") + 1));
4384
+ assertJobsFindArgs(jobArgs);
4385
+ const auth = await resolveFindCommandAuth("dench jobs");
4386
+ await runJobsCommand({
4387
+ convex: auth.convex,
4388
+ args: jobArgs,
4389
+ jsonOutput: json,
4390
+ sessionToken: auth.sessionToken,
4391
+ enrichmentGateway: auth.enrichmentGateway,
4392
+ });
4393
+ return;
4394
+ }
4395
+
4249
4396
  if (command === "members") {
4250
4397
  // Top-level convenience surface for the same Convex query that
4251
4398
  // `dench crm members list` calls. Lives here (rather than only
@@ -4827,6 +4974,23 @@ async function main() {
4827
4974
  }
4828
4975
 
4829
4976
  if (command === "artifacts") {
4977
+ const runtime = await getRuntime();
4978
+ const artifactArgs = stripRuntimeFlags(
4979
+ args.slice(args.indexOf("artifacts") + 1),
4980
+ );
4981
+ if (
4982
+ runtime.mode === "session" ||
4983
+ runtime.mode === "apiKey"
4984
+ ) {
4985
+ const { runWorkspaceArtifactsCommand } = await import("./leads");
4986
+ const handled = await runWorkspaceArtifactsCommand({
4987
+ convex: runtime.client,
4988
+ args: artifactArgs,
4989
+ jsonOutput: json,
4990
+ sessionToken: runtime.sessionToken,
4991
+ });
4992
+ if (handled) return;
4993
+ }
4830
4994
  print(await listArtifacts(runtime, parseNumberOption("--limit")));
4831
4995
  return;
4832
4996
  }
@@ -957,7 +957,7 @@ const enrichmentMetadata = z
957
957
 
958
958
  const enrichedPeopleResponse = z
959
959
  .object({
960
- source: z.enum(["fullenrich", "cache"]),
960
+ source: z.enum(["live", "cache"]),
961
961
  people: z
962
962
  .array(
963
963
  z
@@ -985,7 +985,7 @@ const enrichedPeopleResponse = z
985
985
 
986
986
  const enrichedCompaniesResponse = z
987
987
  .object({
988
- source: z.enum(["fullenrich", "cache"]),
988
+ source: z.enum(["live", "cache"]),
989
989
  companies: z
990
990
  .array(
991
991
  z
@@ -1187,7 +1187,7 @@ const peopleSearch = z.object({
1187
1187
  locations: z.array(z.string()).max(50).optional(),
1188
1188
  headcountRanges: z.array(z.string()).max(20).optional(),
1189
1189
  exactMatch: z.boolean().optional(),
1190
- limit: z.number().int().positive().max(100).optional(),
1190
+ limit: z.number().int().positive().max(5000).optional(),
1191
1191
  offset: z.number().int().nonnegative().optional(),
1192
1192
  preferCache: z.boolean().optional(),
1193
1193
  });
@@ -1197,11 +1197,43 @@ const companySearch = z.object({
1197
1197
  name: z.string().optional(),
1198
1198
  linkedinUrl: z.string().optional(),
1199
1199
  exactMatch: z.boolean().optional(),
1200
- limit: z.number().int().positive().max(100).optional(),
1200
+ limit: z.number().int().positive().max(5000).optional(),
1201
1201
  offset: z.number().int().nonnegative().optional(),
1202
1202
  preferCache: z.boolean().optional(),
1203
1203
  });
1204
1204
 
1205
+ const jobsSearch = z.object({
1206
+ titles: z.array(z.string()).max(50).optional(),
1207
+ locations: z.array(z.string()).max(50).optional(),
1208
+ companyDomain: z.string().optional(),
1209
+ companyName: z.string().optional(),
1210
+ companyLinkedinUrl: z.string().optional(),
1211
+ keywords: z.string().optional(),
1212
+ postedAfter: z.string().optional(),
1213
+ limit: z.number().int().positive().max(5000).optional(),
1214
+ preferCache: z.boolean().optional(),
1215
+ });
1216
+
1217
+ const jobsSearchResponse = z
1218
+ .object({
1219
+ source: z.enum(["live", "cache"]),
1220
+ jobs: z
1221
+ .array(
1222
+ z
1223
+ .object({
1224
+ title: z.string().optional(),
1225
+ company: z.string().optional(),
1226
+ location: z.string().optional(),
1227
+ url: z.string().optional(),
1228
+ workplaceType: z.string().optional(),
1229
+ postedAt: z.string().optional(),
1230
+ })
1231
+ .passthrough(),
1232
+ )
1233
+ .describe("Job postings in the canonical job_posting shape."),
1234
+ })
1235
+ .passthrough();
1236
+
1205
1237
  // ---------------------------------------------------------------------------
1206
1238
  // Request schemas keyed by operation id
1207
1239
  // ---------------------------------------------------------------------------
@@ -1764,12 +1796,10 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
1764
1796
  objectName: z.string(),
1765
1797
  entryId: z.string(),
1766
1798
  fieldName: z.string(),
1767
- provider: z.string().optional().describe("Defaults to auto."),
1768
1799
  }),
1769
1800
  "crm.enrich.object": z.object({
1770
1801
  objectName: z.string(),
1771
1802
  fieldName: z.string(),
1772
- provider: z.string().optional(),
1773
1803
  missingOnly: z
1774
1804
  .boolean()
1775
1805
  .optional()
@@ -1777,6 +1807,31 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
1777
1807
  }),
1778
1808
  "crm.people.search": peopleSearch,
1779
1809
  "crm.companies.search": companySearch,
1810
+ "enrichment.jobs.search": jobsSearch,
1811
+ "enrichment.search.preview": z
1812
+ .object({
1813
+ kind: z.enum(["people", "companies", "jobs"]).optional(),
1814
+ })
1815
+ .passthrough(),
1816
+ "enrichment.person": z
1817
+ .object({
1818
+ linkedinURL: z.string().optional(),
1819
+ linkedinID: z.string().optional(),
1820
+ email: z.string().optional(),
1821
+ })
1822
+ .passthrough(),
1823
+ "enrichment.company": z
1824
+ .object({
1825
+ website: z.string().optional(),
1826
+ linkedinURL: z.string().optional(),
1827
+ linkedinID: z.string().optional(),
1828
+ })
1829
+ .passthrough(),
1830
+ "enrichment.person.contact": z
1831
+ .object({
1832
+ contacts: z.array(z.record(z.string(), z.unknown())).min(1),
1833
+ })
1834
+ .passthrough(),
1780
1835
 
1781
1836
  // Email (sender identities, raw sends, templates, campaigns)
1782
1837
  "email.identity.verify": z.object({
@@ -2485,15 +2540,31 @@ export const responseSchemas: Record<string, ZodTypeAny> = {
2485
2540
  }),
2486
2541
  "crm.enrich.cell": z.object({
2487
2542
  enqueued: z.boolean(),
2488
- provider: z.string(),
2489
2543
  hint: z.string(),
2490
2544
  }),
2491
2545
  "crm.enrich.object": z.object({
2492
2546
  enqueued: z.number().describe("Number of cells queued for enrichment."),
2493
- provider: z.string(),
2494
2547
  }),
2495
2548
  "crm.people.search": enrichedPeopleResponse,
2496
2549
  "crm.companies.search": enrichedCompaniesResponse,
2550
+ "enrichment.jobs.search": jobsSearchResponse,
2551
+ "enrichment.search.preview": z.object({
2552
+ count: z.number(),
2553
+ source: z.enum(["live", "cache"]).optional(),
2554
+ }),
2555
+ "enrichment.person": z
2556
+ .object({
2557
+ source: z.enum(["live", "cache"]),
2558
+ data: z.unknown().nullable(),
2559
+ })
2560
+ .passthrough(),
2561
+ "enrichment.company": z
2562
+ .object({
2563
+ source: z.enum(["live", "cache"]),
2564
+ data: z.unknown().nullable(),
2565
+ })
2566
+ .passthrough(),
2567
+ "enrichment.person.contact": z.object({}).passthrough(),
2497
2568
 
2498
2569
  // Email
2499
2570
  "email.identity.verify": z
@@ -3007,6 +3078,19 @@ export const requestExamples: Record<string, unknown> = {
3007
3078
  limit: 10,
3008
3079
  },
3009
3080
  "crm.companies.search": { domain: "example.com" },
3081
+ "enrichment.jobs.search": {
3082
+ titles: ["Account Executive"],
3083
+ locations: ["San Francisco"],
3084
+ limit: 50,
3085
+ },
3086
+ "enrichment.search.preview": { kind: "people", titles: ["VP Sales"] },
3087
+ "enrichment.person": {
3088
+ linkedinURL: "https://www.linkedin.com/in/janedoe",
3089
+ },
3090
+ "enrichment.company": { website: "acme.com" },
3091
+ "enrichment.person.contact": {
3092
+ contacts: [{ linkedinUrl: "https://www.linkedin.com/in/janedoe" }],
3093
+ },
3010
3094
  "email.identity.verify": { type: "email", identity: "founder@example.com" },
3011
3095
  "email.identity.create": {
3012
3096
  type: "domain",
@@ -3624,12 +3708,11 @@ export const responseExamples: Record<string, unknown> = {
3624
3708
  },
3625
3709
  "crm.enrich.cell": {
3626
3710
  enqueued: true,
3627
- provider: "auto",
3628
- hint: "Enrichment queued; the cell fills in once the provider responds.",
3711
+ hint: "Enrichment queued; the cell fills in once search responds.",
3629
3712
  },
3630
- "crm.enrich.object": { enqueued: 37, provider: "auto" },
3713
+ "crm.enrich.object": { enqueued: 37 },
3631
3714
  "crm.people.search": {
3632
- source: "fullenrich",
3715
+ source: "live",
3633
3716
  people: [
3634
3717
  {
3635
3718
  fullName: "Jane Doe",
@@ -3646,7 +3729,7 @@ export const responseExamples: Record<string, unknown> = {
3646
3729
  metadata: { total: 1, offset: 0 },
3647
3730
  },
3648
3731
  "crm.companies.search": {
3649
- source: "fullenrich",
3732
+ source: "live",
3650
3733
  companies: [
3651
3734
  {
3652
3735
  name: "Acme",
@@ -3660,6 +3743,28 @@ export const responseExamples: Record<string, unknown> = {
3660
3743
  creditsCharged: 1,
3661
3744
  metadata: { total: 1, offset: 0 },
3662
3745
  },
3746
+ "enrichment.jobs.search": {
3747
+ source: "live",
3748
+ jobs: [
3749
+ {
3750
+ title: "Account Executive",
3751
+ company: "Acme",
3752
+ location: "San Francisco",
3753
+ url: "https://example.com/jobs/ae",
3754
+ workplaceType: "hybrid",
3755
+ },
3756
+ ],
3757
+ },
3758
+ "enrichment.search.preview": { count: 247, source: "live" },
3759
+ "enrichment.person": {
3760
+ source: "live",
3761
+ data: { fullName: "Jane Doe", linkedinID: "janedoe" },
3762
+ },
3763
+ "enrichment.company": {
3764
+ source: "live",
3765
+ data: { name: "Acme", website: "acme.com" },
3766
+ },
3767
+ "enrichment.person.contact": { enrichmentId: "job_123", status: "queued" },
3663
3768
  "email.identity.verify": {
3664
3769
  ok: true,
3665
3770
  identity: "example.com",
@@ -1360,7 +1360,7 @@ const rawApiOperations = [
1360
1360
  summary: "Search for people through the enrichment gateway.",
1361
1361
  cli: "dench crm people search",
1362
1362
  method: "POST",
1363
- path: "/crm/people/search",
1363
+ path: "/enrichment/people/search",
1364
1364
  requestSchema: anyObject,
1365
1365
  responseSchema: anyResponse,
1366
1366
  backend: gateway("POST", "/enrichment/people/search"),
@@ -1371,11 +1371,77 @@ const rawApiOperations = [
1371
1371
  summary: "Search for companies through the enrichment gateway.",
1372
1372
  cli: "dench crm companies search",
1373
1373
  method: "POST",
1374
- path: "/crm/companies/search",
1374
+ path: "/enrichment/company/search",
1375
1375
  requestSchema: anyObject,
1376
1376
  responseSchema: anyResponse,
1377
1377
  backend: gateway("POST", "/enrichment/company/search"),
1378
1378
  }),
1379
+ op({
1380
+ id: "enrichment.companies.search",
1381
+ group: "crm",
1382
+ summary: "Search companies through Dench find.",
1383
+ cli: "dench companies find",
1384
+ method: "POST",
1385
+ path: "/enrichment/company/search",
1386
+ requestSchema: anyObject,
1387
+ responseSchema: anyResponse,
1388
+ backend: gateway("POST", "/enrichment/company/search"),
1389
+ }),
1390
+ op({
1391
+ id: "enrichment.jobs.search",
1392
+ group: "crm",
1393
+ summary: "Search job postings through Dench find.",
1394
+ cli: "dench jobs find",
1395
+ method: "POST",
1396
+ path: "/enrichment/jobs/search",
1397
+ requestSchema: anyObject,
1398
+ responseSchema: anyResponse,
1399
+ backend: gateway("POST", "/enrichment/jobs/search"),
1400
+ }),
1401
+ op({
1402
+ id: "enrichment.person",
1403
+ group: "crm",
1404
+ summary: "Look up one person profile.",
1405
+ cli: "dench crm enrich cell",
1406
+ method: "POST",
1407
+ path: "/enrichment/person",
1408
+ requestSchema: anyObject,
1409
+ responseSchema: anyResponse,
1410
+ backend: gateway("POST", "/enrichment/person"),
1411
+ }),
1412
+ op({
1413
+ id: "enrichment.company",
1414
+ group: "crm",
1415
+ summary: "Look up one company profile.",
1416
+ cli: "dench crm enrich cell",
1417
+ method: "POST",
1418
+ path: "/enrichment/company",
1419
+ requestSchema: anyObject,
1420
+ responseSchema: anyResponse,
1421
+ backend: gateway("POST", "/enrichment/company"),
1422
+ }),
1423
+ op({
1424
+ id: "enrichment.person.contact",
1425
+ group: "crm",
1426
+ summary: "Look up verified emails and phones for people.",
1427
+ cli: "dench crm enrich cell",
1428
+ method: "POST",
1429
+ path: "/enrichment/person/contact",
1430
+ requestSchema: anyObject,
1431
+ responseSchema: anyResponse,
1432
+ backend: gateway("POST", "/enrichment/person/contact"),
1433
+ }),
1434
+ op({
1435
+ id: "enrichment.search.preview",
1436
+ group: "crm",
1437
+ summary: "Preview how many records a find would return.",
1438
+ cli: "dench leads find --limit 1",
1439
+ method: "POST",
1440
+ path: "/enrichment/search/preview",
1441
+ requestSchema: anyObject,
1442
+ responseSchema: anyResponse,
1443
+ backend: gateway("POST", "/enrichment/search/preview"),
1444
+ }),
1379
1445
  op({
1380
1446
  id: "crm.templates.list",
1381
1447
  group: "crm",
@@ -27,6 +27,10 @@ export type EnrichmentFieldType =
27
27
  | "tags"
28
28
  | "action";
29
29
 
30
+ /** Contact lookups the gateway can run for a column (person/contact only).
31
+ * Mirrors `EnrichField` in src/lib/workspace/enrichment-columns.ts. */
32
+ export type EnrichField = "work_emails" | "personal_emails" | "phones";
33
+
30
34
  export type EnrichmentColumnDef = {
31
35
  label: string;
32
36
  key: string;
@@ -40,6 +44,9 @@ export type EnrichmentColumnDef = {
40
44
  requiredFields: string[];
41
45
  /** Additional dot-paths to try when apolloPath does not resolve. */
42
46
  extractionFallbacks?: string[];
47
+ /** Contact enrich_fields to request — present only on contact columns
48
+ * (email / phone), which must stay on the /person/contact lane. */
49
+ enrichFields?: EnrichField[];
43
50
  };
44
51
 
45
52
  export type EnrichmentInputKind = "email" | "linkedin" | "domain";
@@ -82,6 +89,7 @@ export const PEOPLE_ENRICHMENT_COLUMNS: readonly EnrichmentColumnDef[] = [
82
89
  apolloPath: "person.email",
83
90
  requiredFields: ["email"],
84
91
  extractionFallbacks: ["email"],
92
+ enrichFields: ["work_emails"],
85
93
  },
86
94
  {
87
95
  label: "Headline",
@@ -114,6 +122,7 @@ export const PEOPLE_ENRICHMENT_COLUMNS: readonly EnrichmentColumnDef[] = [
114
122
  apolloPath: "person.contact.phone_numbers.0.sanitized_number",
115
123
  requiredFields: ["phone"],
116
124
  extractionFallbacks: ["phone", "person.phone"],
125
+ enrichFields: ["phones"],
117
126
  },
118
127
  {
119
128
  label: "Title",
@@ -46,15 +46,75 @@ export type ReverseEmailBody = {
46
46
  email: string;
47
47
  };
48
48
 
49
+ /** One list-filter entry: plain include string or `{value, exclude, exactMatch}`. */
50
+ export type SearchFilterItem =
51
+ | string
52
+ | { value: string; exclude?: boolean; exactMatch?: boolean };
53
+
54
+ export type SearchRangeFilter = { min?: number; max?: number };
55
+
49
56
  export type SearchPeopleBody = {
50
57
  personTitles?: string[];
51
58
  personLocations?: string[];
59
+ personSeniorities?: string[];
60
+ personFunctions?: string[];
52
61
  organizationDomains?: string[];
53
62
  organizationLocations?: string[];
63
+ organizationIndustries?: string[];
54
64
  organizationNumEmployeesRanges?: string[];
55
65
  qKeywords?: string;
66
+ keywords?: string;
56
67
  page?: number;
57
68
  perPage?: number;
69
+ limit?: number;
70
+ offset?: number;
71
+ searchAfter?: unknown;
72
+ // Rich filters, gateway-named. Merged with the legacy person*/organization*
73
+ // aliases above; entries may carry per-value exclude / exactMatch.
74
+ titles?: SearchFilterItem[];
75
+ pastTitles?: SearchFilterItem[];
76
+ personNames?: SearchFilterItem[];
77
+ seniorities?: SearchFilterItem[];
78
+ jobFunctions?: SearchFilterItem[];
79
+ subFunctions?: SearchFilterItem[];
80
+ industries?: SearchFilterItem[];
81
+ locations?: SearchFilterItem[];
82
+ headcountRanges?: SearchFilterItem[];
83
+ companyNames?: SearchFilterItem[];
84
+ pastCompanyNames?: SearchFilterItem[];
85
+ companyDomains?: SearchFilterItem[];
86
+ companyHeadquarters?: SearchFilterItem[];
87
+ companyTypes?: SearchFilterItem[];
88
+ universities?: SearchFilterItem[];
89
+ skills?: SearchFilterItem[];
90
+ languages?: SearchFilterItem[];
91
+ yearsInRole?: SearchRangeFilter;
92
+ yearsAtCompany?: SearchRangeFilter;
93
+ daysSinceJobChange?: SearchRangeFilter;
94
+ foundedYears?: SearchRangeFilter;
95
+ };
96
+
97
+ export type SearchCompaniesBody = {
98
+ domain?: string;
99
+ name?: string;
100
+ linkedinUrl?: string;
101
+ industries?: string[];
102
+ locations?: string[];
103
+ headcountRanges?: string[];
104
+ keywords?: string;
105
+ limit?: number;
106
+ offset?: number;
107
+ };
108
+
109
+ export type SearchJobsBody = {
110
+ titles?: string[];
111
+ locations?: string[];
112
+ companyDomain?: string;
113
+ companyName?: string;
114
+ companyLinkedinUrl?: string;
115
+ keywords?: string;
116
+ postedAfter?: string;
117
+ limit?: number;
58
118
  };
59
119
 
60
120
  export type EnrichmentGatewayOptions = {
@@ -1031,7 +1091,7 @@ export async function enrichCompany(
1031
1091
  }
1032
1092
 
1033
1093
  // ---------------------------------------------------------------------------
1034
- // aviatoEnrichPerson — calls POST /v1/enrichment/aviato/person (synchronous)
1094
+ // Person profile — POST /v1/enrichment/person (synchronous waterfall)
1035
1095
  // ---------------------------------------------------------------------------
1036
1096
 
1037
1097
  export type AviatoEnrichPersonBody = {
@@ -1064,7 +1124,7 @@ export async function aviatoEnrichPerson(
1064
1124
  linkedinUrl || linkedinID || linkedinNumID || linkedinEntityId || email;
1065
1125
  if (!hasIdentifier) {
1066
1126
  throw new EnrichmentGatewayError(
1067
- "Aviato person enrichment requires at least one identifier: linkedinUrl, linkedinID, linkedinNumID, linkedinEntityId, or email.",
1127
+ "Person enrichment requires at least one identifier: linkedinUrl, linkedinID, linkedinNumID, linkedinEntityId, or email.",
1068
1128
  { code: "empty_identifier" },
1069
1129
  );
1070
1130
  }
@@ -1088,7 +1148,7 @@ export async function aviatoEnrichPerson(
1088
1148
  if (body.preview !== undefined && !(body.preview && emailOnly))
1089
1149
  requestBody.preview = body.preview;
1090
1150
 
1091
- const raw = await callGatewayJson("/v1/enrichment/aviato/person", {
1151
+ const raw = await callGatewayJson("/v1/enrichment/person", {
1092
1152
  method: "POST",
1093
1153
  body: requestBody,
1094
1154
  options,
@@ -1110,7 +1170,6 @@ export async function aviatoEnrichPerson(
1110
1170
 
1111
1171
  return {
1112
1172
  _meta: {
1113
- provider: "aviato",
1114
1173
  cacheHit: resp.billing?.cache_hit ?? false,
1115
1174
  chargedCents: resp.billing?.charged_cents ?? 0,
1116
1175
  },
@@ -1119,7 +1178,7 @@ export async function aviatoEnrichPerson(
1119
1178
  }
1120
1179
 
1121
1180
  // ---------------------------------------------------------------------------
1122
- // aviatoEnrichCompany — calls POST /v1/enrichment/aviato/company (synchronous)
1181
+ // Company profile — POST /v1/enrichment/company (synchronous waterfall)
1123
1182
  // ---------------------------------------------------------------------------
1124
1183
 
1125
1184
  export async function aviatoEnrichCompany(
@@ -1130,7 +1189,7 @@ export async function aviatoEnrichCompany(
1130
1189
  body.website || body.linkedinUrl || body.linkedinID || body.linkedinNumID;
1131
1190
  if (!hasIdentifier) {
1132
1191
  throw new EnrichmentGatewayError(
1133
- "Aviato company enrichment requires at least one identifier: website, linkedinUrl, linkedinID, or linkedinNumID.",
1192
+ "Company enrichment requires at least one identifier: website, linkedinUrl, linkedinID, or linkedinNumID.",
1134
1193
  { code: "empty_identifier" },
1135
1194
  );
1136
1195
  }
@@ -1142,7 +1201,7 @@ export async function aviatoEnrichCompany(
1142
1201
  if (body.linkedinNumID) requestBody.linkedinNumID = body.linkedinNumID.trim();
1143
1202
  if (body.preview !== undefined) requestBody.preview = body.preview;
1144
1203
 
1145
- const raw = await callGatewayJson("/v1/enrichment/aviato/company", {
1204
+ const raw = await callGatewayJson("/v1/enrichment/company", {
1146
1205
  method: "POST",
1147
1206
  body: requestBody,
1148
1207
  options,
@@ -1164,7 +1223,6 @@ export async function aviatoEnrichCompany(
1164
1223
 
1165
1224
  return {
1166
1225
  _meta: {
1167
- provider: "aviato",
1168
1226
  cacheHit: resp.billing?.cache_hit ?? false,
1169
1227
  chargedCents: resp.billing?.charged_cents ?? 0,
1170
1228
  },
@@ -1184,29 +1242,79 @@ export async function searchPeople(
1184
1242
  // `locations`, `headcountRanges`, `titles`, `companyDomain`, etc.
1185
1243
  const payload: Record<string, unknown> = { preferCache: true };
1186
1244
 
1187
- if (body.personTitles?.length) payload.titles = body.personTitles;
1245
+ const mergeItems = (
1246
+ legacy: string[] | undefined,
1247
+ rich: SearchFilterItem[] | undefined,
1248
+ ): SearchFilterItem[] => {
1249
+ const legacyItems = (legacy ?? [])
1250
+ .map((s) => s.trim())
1251
+ .filter(Boolean) as SearchFilterItem[];
1252
+ return [...legacyItems, ...(rich ?? [])];
1253
+ };
1254
+
1255
+ const titles = mergeItems(body.personTitles, body.titles);
1256
+ if (titles.length > 0) payload.titles = titles;
1188
1257
 
1189
- const locations = [
1190
- ...(body.personLocations ?? []),
1191
- ...(body.organizationLocations ?? []),
1192
- ]
1193
- .map((s) => s.trim())
1194
- .filter(Boolean);
1258
+ const locations = mergeItems(
1259
+ [...(body.personLocations ?? []), ...(body.organizationLocations ?? [])],
1260
+ body.locations,
1261
+ );
1195
1262
  if (locations.length > 0) payload.locations = locations;
1196
1263
 
1197
1264
  if (body.organizationDomains && body.organizationDomains.length > 0) {
1198
1265
  payload.companyDomain = body.organizationDomains[0];
1199
1266
  }
1200
1267
 
1201
- const headcountRanges = (body.organizationNumEmployeesRanges ?? [])
1202
- .map((s) => s.trim())
1203
- .filter(Boolean);
1204
- if (headcountRanges.length > 0) {
1205
- payload.headcountRanges = headcountRanges;
1268
+ const headcountRanges = mergeItems(
1269
+ body.organizationNumEmployeesRanges,
1270
+ body.headcountRanges,
1271
+ );
1272
+ if (headcountRanges.length > 0) payload.headcountRanges = headcountRanges;
1273
+
1274
+ const seniorities = mergeItems(body.personSeniorities, body.seniorities);
1275
+ if (seniorities.length > 0) payload.seniorities = seniorities;
1276
+ const jobFunctions = mergeItems(body.personFunctions, body.jobFunctions);
1277
+ if (jobFunctions.length > 0) payload.jobFunctions = jobFunctions;
1278
+ const industries = mergeItems(body.organizationIndustries, body.industries);
1279
+ if (industries.length > 0) payload.industries = industries;
1280
+
1281
+ // Rich list filters with no legacy alias — pass straight through.
1282
+ const listPassthrough = [
1283
+ "pastTitles",
1284
+ "personNames",
1285
+ "subFunctions",
1286
+ "companyNames",
1287
+ "pastCompanyNames",
1288
+ "companyDomains",
1289
+ "companyHeadquarters",
1290
+ "companyTypes",
1291
+ "universities",
1292
+ "skills",
1293
+ "languages",
1294
+ ] as const;
1295
+ for (const key of listPassthrough) {
1296
+ const items = body[key];
1297
+ if (items?.length) payload[key] = items;
1298
+ }
1299
+ const rangePassthrough = [
1300
+ "yearsInRole",
1301
+ "yearsAtCompany",
1302
+ "daysSinceJobChange",
1303
+ "foundedYears",
1304
+ ] as const;
1305
+ for (const key of rangePassthrough) {
1306
+ const range = body[key];
1307
+ if (range && (range.min !== undefined || range.max !== undefined)) {
1308
+ payload[key] = range;
1309
+ }
1206
1310
  }
1207
1311
 
1208
- if (body.qKeywords) payload.keywords = body.qKeywords;
1209
- if (body.perPage !== undefined) payload.limit = body.perPage;
1312
+ const keywords = body.keywords ?? body.qKeywords;
1313
+ if (keywords) payload.keywords = keywords;
1314
+ if (body.limit !== undefined) payload.limit = body.limit;
1315
+ else if (body.perPage !== undefined) payload.limit = body.perPage;
1316
+ if (body.offset !== undefined) payload.offset = body.offset;
1317
+ if (body.searchAfter !== undefined) payload.searchAfter = body.searchAfter;
1210
1318
 
1211
1319
  return callGatewayJson("/v1/enrichment/people/search", {
1212
1320
  method: "POST",
@@ -1215,6 +1323,49 @@ export async function searchPeople(
1215
1323
  });
1216
1324
  }
1217
1325
 
1326
+ export async function searchCompanies(
1327
+ body: SearchCompaniesBody,
1328
+ options: EnrichmentGatewayOptions = {},
1329
+ ): Promise<Record<string, unknown>> {
1330
+ const payload: Record<string, unknown> = { preferCache: true };
1331
+ if (body.domain) payload.domain = body.domain;
1332
+ if (body.name) payload.name = body.name;
1333
+ if (body.linkedinUrl) payload.linkedinUrl = body.linkedinUrl;
1334
+ if (body.industries?.length) payload.industries = body.industries;
1335
+ if (body.locations?.length) payload.locations = body.locations;
1336
+ if (body.headcountRanges?.length)
1337
+ payload.headcountRanges = body.headcountRanges;
1338
+ if (body.keywords) payload.keywords = body.keywords;
1339
+ if (body.limit !== undefined) payload.limit = body.limit;
1340
+ if (body.offset !== undefined) payload.offset = body.offset;
1341
+ return callGatewayJson("/v1/enrichment/company/search", {
1342
+ method: "POST",
1343
+ body: payload,
1344
+ options,
1345
+ });
1346
+ }
1347
+
1348
+ export async function searchJobs(
1349
+ body: SearchJobsBody,
1350
+ options: EnrichmentGatewayOptions = {},
1351
+ ): Promise<Record<string, unknown>> {
1352
+ const payload: Record<string, unknown> = {};
1353
+ if (body.titles?.length) payload.titles = body.titles;
1354
+ if (body.locations?.length) payload.locations = body.locations;
1355
+ if (body.companyDomain) payload.companyDomain = body.companyDomain;
1356
+ if (body.companyName) payload.companyName = body.companyName;
1357
+ if (body.companyLinkedinUrl)
1358
+ payload.companyLinkedinUrl = body.companyLinkedinUrl;
1359
+ if (body.keywords) payload.keywords = body.keywords;
1360
+ if (body.postedAfter) payload.postedAfter = body.postedAfter;
1361
+ if (body.limit !== undefined) payload.limit = body.limit;
1362
+ return callGatewayJson("/v1/enrichment/jobs/search", {
1363
+ method: "POST",
1364
+ body: payload,
1365
+ options,
1366
+ });
1367
+ }
1368
+
1218
1369
  /**
1219
1370
  * Number of Exa candidates to pull when resolving ONE person for enrichment.
1220
1371
  *
@@ -1355,7 +1506,8 @@ async function buildGatewayError(
1355
1506
  if (
1356
1507
  response.status === 401 ||
1357
1508
  code === "invalid_api_key" ||
1358
- code === "unauthorized"
1509
+ code === "unauthorized" ||
1510
+ code === "aviato_unauthorized"
1359
1511
  ) {
1360
1512
  return new EnrichmentGatewayError(
1361
1513
  upstreamMessage ?? "Unauthorized gateway request",
@@ -1368,12 +1520,6 @@ async function buildGatewayError(
1368
1520
  { status: response.status, code: "forbidden" },
1369
1521
  );
1370
1522
  }
1371
- if (code === "aviato_unauthorized") {
1372
- return new EnrichmentGatewayError(
1373
- upstreamMessage ?? "Aviato provider unauthorized",
1374
- { status: response.status || 401, code: "aviato_unauthorized" },
1375
- );
1376
- }
1377
1523
  if (response.status === 503 || code === "provider_unavailable") {
1378
1524
  return new EnrichmentGatewayError("Gateway providers unavailable", {
1379
1525
  status: response.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.7.10-staging.100",
3
+ "version": "2.8.0-staging.107",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {