@dench.com/cli 2.7.11-staging.102 → 2.8.1-staging.110
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 +21 -90
- package/dench.ts +165 -4
- package/lib/api-schemas.ts +147 -14
- package/lib/command-registry.ts +68 -2
- package/lib/crm-enrichment.ts +9 -0
- package/lib/enrichment-gateway.ts +184 -29
- package/package.json +1 -1
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
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:
|
|
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] [--
|
|
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] [--page-token <token>] [--json]
|
|
478
|
+
dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--limit 500] [--page-token <token>] [--json]
|
|
479
|
+
dench jobs find --titles "Account Executive" [--locations "San Francisco"] [--limit 500] [--page-token <token>] [--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
|
-
|
|
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] [--page-token <token>] [--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] [--page-token <token>] [--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] [--page-token <token>] [--json]
|
|
743
777
|
`);
|
|
744
778
|
}
|
|
745
779
|
|
|
@@ -2528,6 +2562,7 @@ function stripRuntimeFlags(args: string[]) {
|
|
|
2528
2562
|
"--prod",
|
|
2529
2563
|
"--staging",
|
|
2530
2564
|
"--use-dev-env",
|
|
2565
|
+
"--json",
|
|
2531
2566
|
]);
|
|
2532
2567
|
const valueFlags = new Set(["--convex-url", "--host", "--backend"]);
|
|
2533
2568
|
const stripped: string[] = [];
|
|
@@ -2559,12 +2594,50 @@ function requireAuthenticatedRuntime(
|
|
|
2559
2594
|
throw loginRequiredError(command);
|
|
2560
2595
|
}
|
|
2561
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
|
+
|
|
2562
2628
|
type GatewayCommandAuth = {
|
|
2563
2629
|
bearerToken: string;
|
|
2564
2630
|
gatewayBaseUrl?: string;
|
|
2565
2631
|
gatewayHeaders?: Record<string, string>;
|
|
2566
2632
|
};
|
|
2567
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
|
+
|
|
2568
2641
|
function gatewayBaseUrlForCli(value: unknown): string | undefined {
|
|
2569
2642
|
if (typeof value !== "string") return undefined;
|
|
2570
2643
|
const trimmed = value.trim().replace(/\/+$/, "");
|
|
@@ -2632,7 +2705,10 @@ async function resolveGatewayCommandAuth(
|
|
|
2632
2705
|
command: string,
|
|
2633
2706
|
): Promise<GatewayCommandAuth> {
|
|
2634
2707
|
if (runtime.mode === "apiKey") {
|
|
2635
|
-
return {
|
|
2708
|
+
return {
|
|
2709
|
+
bearerToken: runtime.sessionToken,
|
|
2710
|
+
gatewayBaseUrl: envGatewayBaseUrlOverride(),
|
|
2711
|
+
};
|
|
2636
2712
|
}
|
|
2637
2713
|
|
|
2638
2714
|
if (runtime.mode === "session") {
|
|
@@ -2658,7 +2734,8 @@ async function resolveGatewayCommandAuth(
|
|
|
2658
2734
|
}
|
|
2659
2735
|
return {
|
|
2660
2736
|
bearerToken: gatewayKey,
|
|
2661
|
-
gatewayBaseUrl:
|
|
2737
|
+
gatewayBaseUrl:
|
|
2738
|
+
envGatewayBaseUrlOverride() ?? gatewayBaseUrlForCli(body.gatewayBaseUrl),
|
|
2662
2739
|
gatewayHeaders:
|
|
2663
2740
|
body.gatewayHeaders &&
|
|
2664
2741
|
typeof body.gatewayHeaders === "object" &&
|
|
@@ -2669,7 +2746,12 @@ async function resolveGatewayCommandAuth(
|
|
|
2669
2746
|
}
|
|
2670
2747
|
|
|
2671
2748
|
const envApiKey = process.env.DENCH_API_KEY?.trim();
|
|
2672
|
-
if (envApiKey)
|
|
2749
|
+
if (envApiKey) {
|
|
2750
|
+
return {
|
|
2751
|
+
bearerToken: envApiKey,
|
|
2752
|
+
gatewayBaseUrl: envGatewayBaseUrlOverride(),
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2673
2755
|
throw loginRequiredError(command);
|
|
2674
2756
|
}
|
|
2675
2757
|
|
|
@@ -4169,6 +4251,21 @@ async function main() {
|
|
|
4169
4251
|
return;
|
|
4170
4252
|
}
|
|
4171
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
|
+
|
|
4172
4269
|
if (command === "status" && hasFlag("--help")) {
|
|
4173
4270
|
statusHelp();
|
|
4174
4271
|
return;
|
|
@@ -4249,6 +4346,53 @@ async function main() {
|
|
|
4249
4346
|
return;
|
|
4250
4347
|
}
|
|
4251
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
|
+
|
|
4252
4396
|
if (command === "members") {
|
|
4253
4397
|
// Top-level convenience surface for the same Convex query that
|
|
4254
4398
|
// `dench crm members list` calls. Lives here (rather than only
|
|
@@ -4830,6 +4974,23 @@ async function main() {
|
|
|
4830
4974
|
}
|
|
4831
4975
|
|
|
4832
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
|
+
}
|
|
4833
4994
|
print(await listArtifacts(runtime, parseNumberOption("--limit")));
|
|
4834
4995
|
return;
|
|
4835
4996
|
}
|
package/lib/api-schemas.ts
CHANGED
|
@@ -949,15 +949,30 @@ const toolRunResponse = z
|
|
|
949
949
|
|
|
950
950
|
const enrichmentMetadata = z
|
|
951
951
|
.object({
|
|
952
|
-
total: z
|
|
952
|
+
total: z
|
|
953
|
+
.number()
|
|
954
|
+
.optional()
|
|
955
|
+
.describe(
|
|
956
|
+
"Total matches for the search. Stable across pages; absent when only an estimate exists.",
|
|
957
|
+
),
|
|
958
|
+
totalIsEstimate: z
|
|
959
|
+
.boolean()
|
|
960
|
+
.optional()
|
|
961
|
+
.describe("True when the total is a lower bound rather than exact."),
|
|
953
962
|
offset: z.number().optional(),
|
|
963
|
+
nextPageToken: z
|
|
964
|
+
.string()
|
|
965
|
+
.optional()
|
|
966
|
+
.describe(
|
|
967
|
+
"Opaque token for the next page. Pass it back as pageToken; absent on the last page.",
|
|
968
|
+
),
|
|
954
969
|
})
|
|
955
970
|
.passthrough()
|
|
956
971
|
.optional();
|
|
957
972
|
|
|
958
973
|
const enrichedPeopleResponse = z
|
|
959
974
|
.object({
|
|
960
|
-
source: z.enum(["
|
|
975
|
+
source: z.enum(["live", "cache"]),
|
|
961
976
|
people: z
|
|
962
977
|
.array(
|
|
963
978
|
z
|
|
@@ -985,7 +1000,7 @@ const enrichedPeopleResponse = z
|
|
|
985
1000
|
|
|
986
1001
|
const enrichedCompaniesResponse = z
|
|
987
1002
|
.object({
|
|
988
|
-
source: z.enum(["
|
|
1003
|
+
source: z.enum(["live", "cache"]),
|
|
989
1004
|
companies: z
|
|
990
1005
|
.array(
|
|
991
1006
|
z
|
|
@@ -1187,8 +1202,12 @@ const peopleSearch = z.object({
|
|
|
1187
1202
|
locations: z.array(z.string()).max(50).optional(),
|
|
1188
1203
|
headcountRanges: z.array(z.string()).max(20).optional(),
|
|
1189
1204
|
exactMatch: z.boolean().optional(),
|
|
1190
|
-
limit: z.number().int().positive().max(
|
|
1205
|
+
limit: z.number().int().positive().max(5000).optional(),
|
|
1191
1206
|
offset: z.number().int().nonnegative().optional(),
|
|
1207
|
+
pageToken: z
|
|
1208
|
+
.string()
|
|
1209
|
+
.optional()
|
|
1210
|
+
.describe("Opaque token from a previous response's metadata.nextPageToken."),
|
|
1192
1211
|
preferCache: z.boolean().optional(),
|
|
1193
1212
|
});
|
|
1194
1213
|
|
|
@@ -1197,11 +1216,52 @@ const companySearch = z.object({
|
|
|
1197
1216
|
name: z.string().optional(),
|
|
1198
1217
|
linkedinUrl: z.string().optional(),
|
|
1199
1218
|
exactMatch: z.boolean().optional(),
|
|
1200
|
-
limit: z.number().int().positive().max(
|
|
1219
|
+
limit: z.number().int().positive().max(5000).optional(),
|
|
1201
1220
|
offset: z.number().int().nonnegative().optional(),
|
|
1221
|
+
pageToken: z
|
|
1222
|
+
.string()
|
|
1223
|
+
.optional()
|
|
1224
|
+
.describe("Opaque token from a previous response's metadata.nextPageToken."),
|
|
1202
1225
|
preferCache: z.boolean().optional(),
|
|
1203
1226
|
});
|
|
1204
1227
|
|
|
1228
|
+
const jobsSearch = z.object({
|
|
1229
|
+
titles: z.array(z.string()).max(50).optional(),
|
|
1230
|
+
locations: z.array(z.string()).max(50).optional(),
|
|
1231
|
+
companyDomain: z.string().optional(),
|
|
1232
|
+
companyName: z.string().optional(),
|
|
1233
|
+
companyLinkedinUrl: z.string().optional(),
|
|
1234
|
+
keywords: z.string().optional(),
|
|
1235
|
+
postedAfter: z.string().optional(),
|
|
1236
|
+
limit: z.number().int().positive().max(5000).optional(),
|
|
1237
|
+
pageToken: z
|
|
1238
|
+
.string()
|
|
1239
|
+
.optional()
|
|
1240
|
+
.describe("Opaque token from a previous response's metadata.nextPageToken."),
|
|
1241
|
+
preferCache: z.boolean().optional(),
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const jobsSearchResponse = z
|
|
1245
|
+
.object({
|
|
1246
|
+
source: z.enum(["live", "cache"]),
|
|
1247
|
+
jobs: z
|
|
1248
|
+
.array(
|
|
1249
|
+
z
|
|
1250
|
+
.object({
|
|
1251
|
+
title: z.string().optional(),
|
|
1252
|
+
company: z.string().optional(),
|
|
1253
|
+
location: z.string().optional(),
|
|
1254
|
+
url: z.string().optional(),
|
|
1255
|
+
workplaceType: z.string().optional(),
|
|
1256
|
+
postedAt: z.string().optional(),
|
|
1257
|
+
})
|
|
1258
|
+
.passthrough(),
|
|
1259
|
+
)
|
|
1260
|
+
.describe("Job postings in the canonical job_posting shape."),
|
|
1261
|
+
metadata: enrichmentMetadata,
|
|
1262
|
+
})
|
|
1263
|
+
.passthrough();
|
|
1264
|
+
|
|
1205
1265
|
// ---------------------------------------------------------------------------
|
|
1206
1266
|
// Request schemas keyed by operation id
|
|
1207
1267
|
// ---------------------------------------------------------------------------
|
|
@@ -1764,12 +1824,10 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
|
|
|
1764
1824
|
objectName: z.string(),
|
|
1765
1825
|
entryId: z.string(),
|
|
1766
1826
|
fieldName: z.string(),
|
|
1767
|
-
provider: z.string().optional().describe("Defaults to auto."),
|
|
1768
1827
|
}),
|
|
1769
1828
|
"crm.enrich.object": z.object({
|
|
1770
1829
|
objectName: z.string(),
|
|
1771
1830
|
fieldName: z.string(),
|
|
1772
|
-
provider: z.string().optional(),
|
|
1773
1831
|
missingOnly: z
|
|
1774
1832
|
.boolean()
|
|
1775
1833
|
.optional()
|
|
@@ -1777,6 +1835,31 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
|
|
|
1777
1835
|
}),
|
|
1778
1836
|
"crm.people.search": peopleSearch,
|
|
1779
1837
|
"crm.companies.search": companySearch,
|
|
1838
|
+
"enrichment.jobs.search": jobsSearch,
|
|
1839
|
+
"enrichment.search.preview": z
|
|
1840
|
+
.object({
|
|
1841
|
+
kind: z.enum(["people", "companies", "jobs"]).optional(),
|
|
1842
|
+
})
|
|
1843
|
+
.passthrough(),
|
|
1844
|
+
"enrichment.person": z
|
|
1845
|
+
.object({
|
|
1846
|
+
linkedinURL: z.string().optional(),
|
|
1847
|
+
linkedinID: z.string().optional(),
|
|
1848
|
+
email: z.string().optional(),
|
|
1849
|
+
})
|
|
1850
|
+
.passthrough(),
|
|
1851
|
+
"enrichment.company": z
|
|
1852
|
+
.object({
|
|
1853
|
+
website: z.string().optional(),
|
|
1854
|
+
linkedinURL: z.string().optional(),
|
|
1855
|
+
linkedinID: z.string().optional(),
|
|
1856
|
+
})
|
|
1857
|
+
.passthrough(),
|
|
1858
|
+
"enrichment.person.contact": z
|
|
1859
|
+
.object({
|
|
1860
|
+
contacts: z.array(z.record(z.string(), z.unknown())).min(1),
|
|
1861
|
+
})
|
|
1862
|
+
.passthrough(),
|
|
1780
1863
|
|
|
1781
1864
|
// Email (sender identities, raw sends, templates, campaigns)
|
|
1782
1865
|
"email.identity.verify": z.object({
|
|
@@ -2485,15 +2568,31 @@ export const responseSchemas: Record<string, ZodTypeAny> = {
|
|
|
2485
2568
|
}),
|
|
2486
2569
|
"crm.enrich.cell": z.object({
|
|
2487
2570
|
enqueued: z.boolean(),
|
|
2488
|
-
provider: z.string(),
|
|
2489
2571
|
hint: z.string(),
|
|
2490
2572
|
}),
|
|
2491
2573
|
"crm.enrich.object": z.object({
|
|
2492
2574
|
enqueued: z.number().describe("Number of cells queued for enrichment."),
|
|
2493
|
-
provider: z.string(),
|
|
2494
2575
|
}),
|
|
2495
2576
|
"crm.people.search": enrichedPeopleResponse,
|
|
2496
2577
|
"crm.companies.search": enrichedCompaniesResponse,
|
|
2578
|
+
"enrichment.jobs.search": jobsSearchResponse,
|
|
2579
|
+
"enrichment.search.preview": z.object({
|
|
2580
|
+
count: z.number(),
|
|
2581
|
+
source: z.enum(["live", "cache"]).optional(),
|
|
2582
|
+
}),
|
|
2583
|
+
"enrichment.person": z
|
|
2584
|
+
.object({
|
|
2585
|
+
source: z.enum(["live", "cache"]),
|
|
2586
|
+
data: z.unknown().nullable(),
|
|
2587
|
+
})
|
|
2588
|
+
.passthrough(),
|
|
2589
|
+
"enrichment.company": z
|
|
2590
|
+
.object({
|
|
2591
|
+
source: z.enum(["live", "cache"]),
|
|
2592
|
+
data: z.unknown().nullable(),
|
|
2593
|
+
})
|
|
2594
|
+
.passthrough(),
|
|
2595
|
+
"enrichment.person.contact": z.object({}).passthrough(),
|
|
2497
2596
|
|
|
2498
2597
|
// Email
|
|
2499
2598
|
"email.identity.verify": z
|
|
@@ -3007,6 +3106,19 @@ export const requestExamples: Record<string, unknown> = {
|
|
|
3007
3106
|
limit: 10,
|
|
3008
3107
|
},
|
|
3009
3108
|
"crm.companies.search": { domain: "example.com" },
|
|
3109
|
+
"enrichment.jobs.search": {
|
|
3110
|
+
titles: ["Account Executive"],
|
|
3111
|
+
locations: ["San Francisco"],
|
|
3112
|
+
limit: 50,
|
|
3113
|
+
},
|
|
3114
|
+
"enrichment.search.preview": { kind: "people", titles: ["VP Sales"] },
|
|
3115
|
+
"enrichment.person": {
|
|
3116
|
+
linkedinURL: "https://www.linkedin.com/in/janedoe",
|
|
3117
|
+
},
|
|
3118
|
+
"enrichment.company": { website: "acme.com" },
|
|
3119
|
+
"enrichment.person.contact": {
|
|
3120
|
+
contacts: [{ linkedinUrl: "https://www.linkedin.com/in/janedoe" }],
|
|
3121
|
+
},
|
|
3010
3122
|
"email.identity.verify": { type: "email", identity: "founder@example.com" },
|
|
3011
3123
|
"email.identity.create": {
|
|
3012
3124
|
type: "domain",
|
|
@@ -3624,12 +3736,11 @@ export const responseExamples: Record<string, unknown> = {
|
|
|
3624
3736
|
},
|
|
3625
3737
|
"crm.enrich.cell": {
|
|
3626
3738
|
enqueued: true,
|
|
3627
|
-
|
|
3628
|
-
hint: "Enrichment queued; the cell fills in once the provider responds.",
|
|
3739
|
+
hint: "Enrichment queued; the cell fills in once search responds.",
|
|
3629
3740
|
},
|
|
3630
|
-
"crm.enrich.object": { enqueued: 37
|
|
3741
|
+
"crm.enrich.object": { enqueued: 37 },
|
|
3631
3742
|
"crm.people.search": {
|
|
3632
|
-
source: "
|
|
3743
|
+
source: "live",
|
|
3633
3744
|
people: [
|
|
3634
3745
|
{
|
|
3635
3746
|
fullName: "Jane Doe",
|
|
@@ -3646,7 +3757,7 @@ export const responseExamples: Record<string, unknown> = {
|
|
|
3646
3757
|
metadata: { total: 1, offset: 0 },
|
|
3647
3758
|
},
|
|
3648
3759
|
"crm.companies.search": {
|
|
3649
|
-
source: "
|
|
3760
|
+
source: "live",
|
|
3650
3761
|
companies: [
|
|
3651
3762
|
{
|
|
3652
3763
|
name: "Acme",
|
|
@@ -3660,6 +3771,28 @@ export const responseExamples: Record<string, unknown> = {
|
|
|
3660
3771
|
creditsCharged: 1,
|
|
3661
3772
|
metadata: { total: 1, offset: 0 },
|
|
3662
3773
|
},
|
|
3774
|
+
"enrichment.jobs.search": {
|
|
3775
|
+
source: "live",
|
|
3776
|
+
jobs: [
|
|
3777
|
+
{
|
|
3778
|
+
title: "Account Executive",
|
|
3779
|
+
company: "Acme",
|
|
3780
|
+
location: "San Francisco",
|
|
3781
|
+
url: "https://example.com/jobs/ae",
|
|
3782
|
+
workplaceType: "hybrid",
|
|
3783
|
+
},
|
|
3784
|
+
],
|
|
3785
|
+
},
|
|
3786
|
+
"enrichment.search.preview": { count: 247, source: "live" },
|
|
3787
|
+
"enrichment.person": {
|
|
3788
|
+
source: "live",
|
|
3789
|
+
data: { fullName: "Jane Doe", linkedinID: "janedoe" },
|
|
3790
|
+
},
|
|
3791
|
+
"enrichment.company": {
|
|
3792
|
+
source: "live",
|
|
3793
|
+
data: { name: "Acme", website: "acme.com" },
|
|
3794
|
+
},
|
|
3795
|
+
"enrichment.person.contact": { enrichmentId: "job_123", status: "queued" },
|
|
3663
3796
|
"email.identity.verify": {
|
|
3664
3797
|
ok: true,
|
|
3665
3798
|
identity: "example.com",
|
package/lib/command-registry.ts
CHANGED
|
@@ -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: "/
|
|
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: "/
|
|
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",
|
package/lib/crm-enrichment.ts
CHANGED
|
@@ -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,81 @@ 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
|
+
/** Opaque token from a previous response's `metadata.nextPageToken`. */
|
|
73
|
+
pageToken?: string;
|
|
74
|
+
// Rich filters, gateway-named. Merged with the legacy person*/organization*
|
|
75
|
+
// aliases above; entries may carry per-value exclude / exactMatch.
|
|
76
|
+
titles?: SearchFilterItem[];
|
|
77
|
+
pastTitles?: SearchFilterItem[];
|
|
78
|
+
personNames?: SearchFilterItem[];
|
|
79
|
+
seniorities?: SearchFilterItem[];
|
|
80
|
+
jobFunctions?: SearchFilterItem[];
|
|
81
|
+
subFunctions?: SearchFilterItem[];
|
|
82
|
+
industries?: SearchFilterItem[];
|
|
83
|
+
locations?: SearchFilterItem[];
|
|
84
|
+
headcountRanges?: SearchFilterItem[];
|
|
85
|
+
companyNames?: SearchFilterItem[];
|
|
86
|
+
pastCompanyNames?: SearchFilterItem[];
|
|
87
|
+
companyDomains?: SearchFilterItem[];
|
|
88
|
+
companyHeadquarters?: SearchFilterItem[];
|
|
89
|
+
companyTypes?: SearchFilterItem[];
|
|
90
|
+
universities?: SearchFilterItem[];
|
|
91
|
+
skills?: SearchFilterItem[];
|
|
92
|
+
languages?: SearchFilterItem[];
|
|
93
|
+
yearsInRole?: SearchRangeFilter;
|
|
94
|
+
yearsAtCompany?: SearchRangeFilter;
|
|
95
|
+
daysSinceJobChange?: SearchRangeFilter;
|
|
96
|
+
foundedYears?: SearchRangeFilter;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type SearchCompaniesBody = {
|
|
100
|
+
domain?: string;
|
|
101
|
+
name?: string;
|
|
102
|
+
linkedinUrl?: string;
|
|
103
|
+
industries?: string[];
|
|
104
|
+
locations?: string[];
|
|
105
|
+
headcountRanges?: string[];
|
|
106
|
+
keywords?: string;
|
|
107
|
+
limit?: number;
|
|
108
|
+
offset?: number;
|
|
109
|
+
/** Opaque token from a previous response's `metadata.nextPageToken`. */
|
|
110
|
+
pageToken?: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type SearchJobsBody = {
|
|
114
|
+
titles?: string[];
|
|
115
|
+
locations?: string[];
|
|
116
|
+
companyDomain?: string;
|
|
117
|
+
companyName?: string;
|
|
118
|
+
companyLinkedinUrl?: string;
|
|
119
|
+
keywords?: string;
|
|
120
|
+
postedAfter?: string;
|
|
121
|
+
limit?: number;
|
|
122
|
+
/** Opaque token from a previous response's `metadata.nextPageToken`. */
|
|
123
|
+
pageToken?: string;
|
|
58
124
|
};
|
|
59
125
|
|
|
60
126
|
export type EnrichmentGatewayOptions = {
|
|
@@ -1031,7 +1097,7 @@ export async function enrichCompany(
|
|
|
1031
1097
|
}
|
|
1032
1098
|
|
|
1033
1099
|
// ---------------------------------------------------------------------------
|
|
1034
|
-
//
|
|
1100
|
+
// Person profile — POST /v1/enrichment/person (synchronous waterfall)
|
|
1035
1101
|
// ---------------------------------------------------------------------------
|
|
1036
1102
|
|
|
1037
1103
|
export type AviatoEnrichPersonBody = {
|
|
@@ -1064,7 +1130,7 @@ export async function aviatoEnrichPerson(
|
|
|
1064
1130
|
linkedinUrl || linkedinID || linkedinNumID || linkedinEntityId || email;
|
|
1065
1131
|
if (!hasIdentifier) {
|
|
1066
1132
|
throw new EnrichmentGatewayError(
|
|
1067
|
-
"
|
|
1133
|
+
"Person enrichment requires at least one identifier: linkedinUrl, linkedinID, linkedinNumID, linkedinEntityId, or email.",
|
|
1068
1134
|
{ code: "empty_identifier" },
|
|
1069
1135
|
);
|
|
1070
1136
|
}
|
|
@@ -1088,7 +1154,7 @@ export async function aviatoEnrichPerson(
|
|
|
1088
1154
|
if (body.preview !== undefined && !(body.preview && emailOnly))
|
|
1089
1155
|
requestBody.preview = body.preview;
|
|
1090
1156
|
|
|
1091
|
-
const raw = await callGatewayJson("/v1/enrichment/
|
|
1157
|
+
const raw = await callGatewayJson("/v1/enrichment/person", {
|
|
1092
1158
|
method: "POST",
|
|
1093
1159
|
body: requestBody,
|
|
1094
1160
|
options,
|
|
@@ -1110,7 +1176,6 @@ export async function aviatoEnrichPerson(
|
|
|
1110
1176
|
|
|
1111
1177
|
return {
|
|
1112
1178
|
_meta: {
|
|
1113
|
-
provider: "aviato",
|
|
1114
1179
|
cacheHit: resp.billing?.cache_hit ?? false,
|
|
1115
1180
|
chargedCents: resp.billing?.charged_cents ?? 0,
|
|
1116
1181
|
},
|
|
@@ -1119,7 +1184,7 @@ export async function aviatoEnrichPerson(
|
|
|
1119
1184
|
}
|
|
1120
1185
|
|
|
1121
1186
|
// ---------------------------------------------------------------------------
|
|
1122
|
-
//
|
|
1187
|
+
// Company profile — POST /v1/enrichment/company (synchronous waterfall)
|
|
1123
1188
|
// ---------------------------------------------------------------------------
|
|
1124
1189
|
|
|
1125
1190
|
export async function aviatoEnrichCompany(
|
|
@@ -1130,7 +1195,7 @@ export async function aviatoEnrichCompany(
|
|
|
1130
1195
|
body.website || body.linkedinUrl || body.linkedinID || body.linkedinNumID;
|
|
1131
1196
|
if (!hasIdentifier) {
|
|
1132
1197
|
throw new EnrichmentGatewayError(
|
|
1133
|
-
"
|
|
1198
|
+
"Company enrichment requires at least one identifier: website, linkedinUrl, linkedinID, or linkedinNumID.",
|
|
1134
1199
|
{ code: "empty_identifier" },
|
|
1135
1200
|
);
|
|
1136
1201
|
}
|
|
@@ -1142,7 +1207,7 @@ export async function aviatoEnrichCompany(
|
|
|
1142
1207
|
if (body.linkedinNumID) requestBody.linkedinNumID = body.linkedinNumID.trim();
|
|
1143
1208
|
if (body.preview !== undefined) requestBody.preview = body.preview;
|
|
1144
1209
|
|
|
1145
|
-
const raw = await callGatewayJson("/v1/enrichment/
|
|
1210
|
+
const raw = await callGatewayJson("/v1/enrichment/company", {
|
|
1146
1211
|
method: "POST",
|
|
1147
1212
|
body: requestBody,
|
|
1148
1213
|
options,
|
|
@@ -1164,7 +1229,6 @@ export async function aviatoEnrichCompany(
|
|
|
1164
1229
|
|
|
1165
1230
|
return {
|
|
1166
1231
|
_meta: {
|
|
1167
|
-
provider: "aviato",
|
|
1168
1232
|
cacheHit: resp.billing?.cache_hit ?? false,
|
|
1169
1233
|
chargedCents: resp.billing?.charged_cents ?? 0,
|
|
1170
1234
|
},
|
|
@@ -1184,29 +1248,80 @@ export async function searchPeople(
|
|
|
1184
1248
|
// `locations`, `headcountRanges`, `titles`, `companyDomain`, etc.
|
|
1185
1249
|
const payload: Record<string, unknown> = { preferCache: true };
|
|
1186
1250
|
|
|
1187
|
-
|
|
1251
|
+
const mergeItems = (
|
|
1252
|
+
legacy: string[] | undefined,
|
|
1253
|
+
rich: SearchFilterItem[] | undefined,
|
|
1254
|
+
): SearchFilterItem[] => {
|
|
1255
|
+
const legacyItems = (legacy ?? [])
|
|
1256
|
+
.map((s) => s.trim())
|
|
1257
|
+
.filter(Boolean) as SearchFilterItem[];
|
|
1258
|
+
return [...legacyItems, ...(rich ?? [])];
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
const titles = mergeItems(body.personTitles, body.titles);
|
|
1262
|
+
if (titles.length > 0) payload.titles = titles;
|
|
1188
1263
|
|
|
1189
|
-
const locations =
|
|
1190
|
-
...(body.personLocations ?? []),
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
.map((s) => s.trim())
|
|
1194
|
-
.filter(Boolean);
|
|
1264
|
+
const locations = mergeItems(
|
|
1265
|
+
[...(body.personLocations ?? []), ...(body.organizationLocations ?? [])],
|
|
1266
|
+
body.locations,
|
|
1267
|
+
);
|
|
1195
1268
|
if (locations.length > 0) payload.locations = locations;
|
|
1196
1269
|
|
|
1197
1270
|
if (body.organizationDomains && body.organizationDomains.length > 0) {
|
|
1198
1271
|
payload.companyDomain = body.organizationDomains[0];
|
|
1199
1272
|
}
|
|
1200
1273
|
|
|
1201
|
-
const headcountRanges = (
|
|
1202
|
-
.
|
|
1203
|
-
.
|
|
1204
|
-
|
|
1205
|
-
|
|
1274
|
+
const headcountRanges = mergeItems(
|
|
1275
|
+
body.organizationNumEmployeesRanges,
|
|
1276
|
+
body.headcountRanges,
|
|
1277
|
+
);
|
|
1278
|
+
if (headcountRanges.length > 0) payload.headcountRanges = headcountRanges;
|
|
1279
|
+
|
|
1280
|
+
const seniorities = mergeItems(body.personSeniorities, body.seniorities);
|
|
1281
|
+
if (seniorities.length > 0) payload.seniorities = seniorities;
|
|
1282
|
+
const jobFunctions = mergeItems(body.personFunctions, body.jobFunctions);
|
|
1283
|
+
if (jobFunctions.length > 0) payload.jobFunctions = jobFunctions;
|
|
1284
|
+
const industries = mergeItems(body.organizationIndustries, body.industries);
|
|
1285
|
+
if (industries.length > 0) payload.industries = industries;
|
|
1286
|
+
|
|
1287
|
+
// Rich list filters with no legacy alias — pass straight through.
|
|
1288
|
+
const listPassthrough = [
|
|
1289
|
+
"pastTitles",
|
|
1290
|
+
"personNames",
|
|
1291
|
+
"subFunctions",
|
|
1292
|
+
"companyNames",
|
|
1293
|
+
"pastCompanyNames",
|
|
1294
|
+
"companyDomains",
|
|
1295
|
+
"companyHeadquarters",
|
|
1296
|
+
"companyTypes",
|
|
1297
|
+
"universities",
|
|
1298
|
+
"skills",
|
|
1299
|
+
"languages",
|
|
1300
|
+
] as const;
|
|
1301
|
+
for (const key of listPassthrough) {
|
|
1302
|
+
const items = body[key];
|
|
1303
|
+
if (items?.length) payload[key] = items;
|
|
1304
|
+
}
|
|
1305
|
+
const rangePassthrough = [
|
|
1306
|
+
"yearsInRole",
|
|
1307
|
+
"yearsAtCompany",
|
|
1308
|
+
"daysSinceJobChange",
|
|
1309
|
+
"foundedYears",
|
|
1310
|
+
] as const;
|
|
1311
|
+
for (const key of rangePassthrough) {
|
|
1312
|
+
const range = body[key];
|
|
1313
|
+
if (range && (range.min !== undefined || range.max !== undefined)) {
|
|
1314
|
+
payload[key] = range;
|
|
1315
|
+
}
|
|
1206
1316
|
}
|
|
1207
1317
|
|
|
1208
|
-
|
|
1209
|
-
if (
|
|
1318
|
+
const keywords = body.keywords ?? body.qKeywords;
|
|
1319
|
+
if (keywords) payload.keywords = keywords;
|
|
1320
|
+
if (body.limit !== undefined) payload.limit = body.limit;
|
|
1321
|
+
else if (body.perPage !== undefined) payload.limit = body.perPage;
|
|
1322
|
+
if (body.offset !== undefined) payload.offset = body.offset;
|
|
1323
|
+
if (body.searchAfter !== undefined) payload.searchAfter = body.searchAfter;
|
|
1324
|
+
if (body.pageToken) payload.pageToken = body.pageToken;
|
|
1210
1325
|
|
|
1211
1326
|
return callGatewayJson("/v1/enrichment/people/search", {
|
|
1212
1327
|
method: "POST",
|
|
@@ -1215,6 +1330,51 @@ export async function searchPeople(
|
|
|
1215
1330
|
});
|
|
1216
1331
|
}
|
|
1217
1332
|
|
|
1333
|
+
export async function searchCompanies(
|
|
1334
|
+
body: SearchCompaniesBody,
|
|
1335
|
+
options: EnrichmentGatewayOptions = {},
|
|
1336
|
+
): Promise<Record<string, unknown>> {
|
|
1337
|
+
const payload: Record<string, unknown> = { preferCache: true };
|
|
1338
|
+
if (body.domain) payload.domain = body.domain;
|
|
1339
|
+
if (body.name) payload.name = body.name;
|
|
1340
|
+
if (body.linkedinUrl) payload.linkedinUrl = body.linkedinUrl;
|
|
1341
|
+
if (body.industries?.length) payload.industries = body.industries;
|
|
1342
|
+
if (body.locations?.length) payload.locations = body.locations;
|
|
1343
|
+
if (body.headcountRanges?.length)
|
|
1344
|
+
payload.headcountRanges = body.headcountRanges;
|
|
1345
|
+
if (body.keywords) payload.keywords = body.keywords;
|
|
1346
|
+
if (body.limit !== undefined) payload.limit = body.limit;
|
|
1347
|
+
if (body.offset !== undefined) payload.offset = body.offset;
|
|
1348
|
+
if (body.pageToken) payload.pageToken = body.pageToken;
|
|
1349
|
+
return callGatewayJson("/v1/enrichment/company/search", {
|
|
1350
|
+
method: "POST",
|
|
1351
|
+
body: payload,
|
|
1352
|
+
options,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
export async function searchJobs(
|
|
1357
|
+
body: SearchJobsBody,
|
|
1358
|
+
options: EnrichmentGatewayOptions = {},
|
|
1359
|
+
): Promise<Record<string, unknown>> {
|
|
1360
|
+
const payload: Record<string, unknown> = {};
|
|
1361
|
+
if (body.titles?.length) payload.titles = body.titles;
|
|
1362
|
+
if (body.locations?.length) payload.locations = body.locations;
|
|
1363
|
+
if (body.companyDomain) payload.companyDomain = body.companyDomain;
|
|
1364
|
+
if (body.companyName) payload.companyName = body.companyName;
|
|
1365
|
+
if (body.companyLinkedinUrl)
|
|
1366
|
+
payload.companyLinkedinUrl = body.companyLinkedinUrl;
|
|
1367
|
+
if (body.keywords) payload.keywords = body.keywords;
|
|
1368
|
+
if (body.postedAfter) payload.postedAfter = body.postedAfter;
|
|
1369
|
+
if (body.limit !== undefined) payload.limit = body.limit;
|
|
1370
|
+
if (body.pageToken) payload.pageToken = body.pageToken;
|
|
1371
|
+
return callGatewayJson("/v1/enrichment/jobs/search", {
|
|
1372
|
+
method: "POST",
|
|
1373
|
+
body: payload,
|
|
1374
|
+
options,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1218
1378
|
/**
|
|
1219
1379
|
* Number of Exa candidates to pull when resolving ONE person for enrichment.
|
|
1220
1380
|
*
|
|
@@ -1355,7 +1515,8 @@ async function buildGatewayError(
|
|
|
1355
1515
|
if (
|
|
1356
1516
|
response.status === 401 ||
|
|
1357
1517
|
code === "invalid_api_key" ||
|
|
1358
|
-
code === "unauthorized"
|
|
1518
|
+
code === "unauthorized" ||
|
|
1519
|
+
code === "aviato_unauthorized"
|
|
1359
1520
|
) {
|
|
1360
1521
|
return new EnrichmentGatewayError(
|
|
1361
1522
|
upstreamMessage ?? "Unauthorized gateway request",
|
|
@@ -1368,12 +1529,6 @@ async function buildGatewayError(
|
|
|
1368
1529
|
{ status: response.status, code: "forbidden" },
|
|
1369
1530
|
);
|
|
1370
1531
|
}
|
|
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
1532
|
if (response.status === 503 || code === "provider_unavailable") {
|
|
1378
1533
|
return new EnrichmentGatewayError("Gateway providers unavailable", {
|
|
1379
1534
|
status: response.status,
|