@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 +21 -90
- package/dench.ts +171 -7
- package/lib/api-schemas.ts +118 -13
- package/lib/command-registry.ts +68 -2
- package/lib/crm-enrichment.ts +9 -0
- package/lib/enrichment-gateway.ts +175 -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] [--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
|
-
|
|
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
|
|
827
|
-
|
|
828
|
-
|
|
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 {
|
|
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:
|
|
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)
|
|
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
|
}
|
package/lib/api-schemas.ts
CHANGED
|
@@ -957,7 +957,7 @@ const enrichmentMetadata = z
|
|
|
957
957
|
|
|
958
958
|
const enrichedPeopleResponse = z
|
|
959
959
|
.object({
|
|
960
|
-
source: z.enum(["
|
|
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(["
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
3713
|
+
"crm.enrich.object": { enqueued: 37 },
|
|
3631
3714
|
"crm.people.search": {
|
|
3632
|
-
source: "
|
|
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: "
|
|
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",
|
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,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
|
-
//
|
|
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
|
-
"
|
|
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/
|
|
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
|
-
//
|
|
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
|
-
"
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
1202
|
-
.
|
|
1203
|
-
.
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1209
|
-
if (
|
|
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,
|