@dench.com/cli 2.8.0-staging.107 → 2.8.2-staging.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/crm.ts CHANGED
@@ -3093,6 +3093,13 @@ async function resolveEnrichmentTarget(
3093
3093
  * Email (no first/last name reached the gateway, so the validator
3094
3094
  * rejected the request before any lookup ran).
3095
3095
  */
3096
+ const CONTACT_REQUIRED_FIELDS = new Set([
3097
+ "email",
3098
+ "work_email",
3099
+ "personal_email",
3100
+ "phone",
3101
+ ]);
3102
+
3096
3103
  async function callEnrichmentGatewayForEntry(args: {
3097
3104
  target: EnrichmentTarget;
3098
3105
  entry: CrmEntryForEnrichment;
@@ -3118,7 +3125,11 @@ async function callEnrichmentGatewayForEntry(args: {
3118
3125
  // Contact cells stay on /person/contact. A LinkedIn URL for a profile
3119
3126
  // field (name, title, headline) uses /person so the gateway waterfall
3120
3127
  // can fill graph data without a contact job.
3121
- const isContactField = Boolean(target.column.enrichFields?.length);
3128
+ const isContactField =
3129
+ Boolean(target.column.enrichFields?.length) ||
3130
+ target.column.requiredFields.some((field) =>
3131
+ CONTACT_REQUIRED_FIELDS.has(field),
3132
+ );
3122
3133
  if (!isContactField && personBody.linkedinUrl) {
3123
3134
  return aviatoEnrichPerson(
3124
3135
  {
package/dench.ts CHANGED
@@ -44,7 +44,11 @@ import {
44
44
  withCurrentSessionSelection,
45
45
  withSavedSession,
46
46
  } from "./session";
47
- import { fetchConnectedAppsSummary, runToolCommand } from "./tools";
47
+ import {
48
+ fetchConnectedAppsSummary,
49
+ fetchUnipileConnections,
50
+ runToolCommand,
51
+ } from "./tools";
48
52
 
49
53
  type JsonRecord = Record<string, unknown>;
50
54
 
@@ -474,9 +478,9 @@ Usage:
474
478
  dench artifacts list [--limit 25] [--json]
475
479
  dench artifacts show <id> [--json]
476
480
  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]
481
+ dench leads find --titles "VP Sales" [--locations "San Francisco"] [--limit 500] [--page-token <token>] [--json]
482
+ dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--limit 500] [--page-token <token>] [--json]
483
+ dench jobs find --titles "Account Executive" [--locations "San Francisco"] [--limit 500] [--page-token <token>] [--json]
480
484
  dench suggested-work [--json]
481
485
  dench approval request "message" [--json]
482
486
  dench approval approve <approvalId> [--evidence "User said yes in chat"] [--json]
@@ -757,7 +761,7 @@ function leadsHelp() {
757
761
  console.log(`Dench leads
758
762
 
759
763
  Usage:
760
- dench leads find --titles "VP Sales" --locations "San Francisco" [--company-domain acme.com] [--keywords fintech] [--limit 500] [--json]
764
+ dench leads find --titles "VP Sales" --locations "San Francisco" [--company-domain acme.com] [--keywords fintech] [--limit 500] [--page-token <token>] [--json]
761
765
  `);
762
766
  }
763
767
 
@@ -765,7 +769,7 @@ function companiesHelp() {
765
769
  console.log(`Dench companies
766
770
 
767
771
  Usage:
768
- dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--industries SaaS] [--locations "San Francisco"] [--limit 500] [--json]
772
+ dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--industries SaaS] [--locations "San Francisco"] [--limit 500] [--page-token <token>] [--json]
769
773
  `);
770
774
  }
771
775
 
@@ -773,7 +777,7 @@ function jobsHelp() {
773
777
  console.log(`Dench jobs
774
778
 
775
779
  Usage:
776
- dench jobs find --titles "Account Executive" --locations "San Francisco" [--company-domain acme.com] [--keywords remote] [--limit 500] [--json]
780
+ dench jobs find --titles "Account Executive" --locations "San Francisco" [--company-domain acme.com] [--keywords remote] [--limit 500] [--page-token <token>] [--json]
777
781
  `);
778
782
  }
779
783
 
@@ -2879,11 +2883,14 @@ async function connectedAppsContext(runtime: Runtime) {
2879
2883
  ...unipileQueryAuth(runtime),
2880
2884
  });
2881
2885
  } catch (error) {
2886
+ const unipile = await fetchUnipileConnections(unipileQueryAuth(runtime));
2882
2887
  return {
2883
2888
  available: false,
2884
2889
  reason: errorMessage(error),
2885
- count: 0,
2886
- connections: [],
2890
+ count: unipile.connections.length,
2891
+ connections: unipile.connections,
2892
+ unipileAvailable: unipile.available,
2893
+ ...(unipile.error ? { unipileError: unipile.error } : {}),
2887
2894
  };
2888
2895
  }
2889
2896
  }
@@ -4979,6 +4986,7 @@ async function main() {
4979
4986
  args.slice(args.indexOf("artifacts") + 1),
4980
4987
  );
4981
4988
  if (
4989
+ runtime.mode === "dev" ||
4982
4990
  runtime.mode === "session" ||
4983
4991
  runtime.mode === "apiKey"
4984
4992
  ) {
@@ -4987,7 +4995,10 @@ async function main() {
4987
4995
  convex: runtime.client,
4988
4996
  args: artifactArgs,
4989
4997
  jsonOutput: json,
4990
- sessionToken: runtime.sessionToken,
4998
+ sessionToken:
4999
+ runtime.mode === "dev"
5000
+ ? encodeDevCrmSessionToken(runtime.workspaceArgs)
5001
+ : runtime.sessionToken,
4991
5002
  });
4992
5003
  if (handled) return;
4993
5004
  }
package/leads.ts ADDED
@@ -0,0 +1,585 @@
1
+ /**
2
+ * `dench leads find`, `dench jobs find`, and workspace artifact
3
+ * list/show/import. Find writes a durable table artifact; import is the
4
+ * same path as the chat card.
5
+ */
6
+ import type { ConvexHttpClient } from "convex/browser";
7
+ import { makeFunctionReference } from "convex/server";
8
+ import {
9
+ assertNoUnknownFlags as assertNoUnknownFlagsRaw,
10
+ CliArgError,
11
+ getFlag,
12
+ hasFlag,
13
+ shift as shiftRaw,
14
+ } from "./lib/cli-args";
15
+ import {
16
+ EnrichmentGatewayError,
17
+ type EnrichmentGatewayOptions,
18
+ searchCompanies,
19
+ searchJobs,
20
+ searchPeople,
21
+ } from "./lib/enrichment-gateway";
22
+
23
+ class FindCliError extends Error {
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.name = "FindCliError";
27
+ }
28
+ }
29
+
30
+ type FindCliContext = {
31
+ convex: ConvexHttpClient;
32
+ args: string[];
33
+ jsonOutput: boolean;
34
+ sessionToken?: string;
35
+ enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
36
+ };
37
+
38
+ const api = {
39
+ saveFind: makeFunctionReference<"action">(
40
+ "functions/workspaceArtifacts:saveFind",
41
+ ),
42
+ list: makeFunctionReference<"query">("functions/workspaceArtifacts:list"),
43
+ get: makeFunctionReference<"query">("functions/workspaceArtifacts:get"),
44
+ importArtifact: makeFunctionReference<"action">(
45
+ "functions/workspaceArtifacts:importArtifact",
46
+ ),
47
+ };
48
+
49
+ function shift(args: string[], expected: string): string {
50
+ try {
51
+ return shiftRaw(args, expected);
52
+ } catch (error) {
53
+ if (error instanceof CliArgError) throw new FindCliError(error.message);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ function assertNoUnknownFlags(args: readonly string[], command: string): void {
59
+ try {
60
+ assertNoUnknownFlagsRaw(args, command);
61
+ } catch (error) {
62
+ if (error instanceof CliArgError) throw new FindCliError(error.message);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ function parseLeadsFindFlags(args: string[]): {
68
+ titles?: string[];
69
+ locations?: string[];
70
+ seniorities?: string[];
71
+ industries?: string[];
72
+ functions?: string[];
73
+ companyDomain?: string;
74
+ keywords?: string;
75
+ limit?: number;
76
+ pageToken?: string;
77
+ } {
78
+ const titles = csvFlag(args, "--titles");
79
+ const locations = csvFlag(args, "--locations");
80
+ const seniorities = csvFlag(args, "--seniorities");
81
+ const industries = csvFlag(args, "--industries");
82
+ const functions = csvFlag(args, "--functions");
83
+ const companyDomain = getFlag(args, "--company-domain");
84
+ const keywords = getFlag(args, "--keywords");
85
+ const limit = parseLimit(args);
86
+ const pageToken = getFlag(args, "--page-token");
87
+ assertNoUnknownFlags(args, "leads find");
88
+ if (
89
+ !titles &&
90
+ !locations &&
91
+ !companyDomain &&
92
+ !keywords &&
93
+ !industries &&
94
+ !seniorities
95
+ ) {
96
+ throw new FindCliError(
97
+ "dench leads find requires at least one of --titles, --locations, --company-domain, --keywords, --industries, --seniorities",
98
+ );
99
+ }
100
+ return {
101
+ titles,
102
+ locations,
103
+ seniorities,
104
+ industries,
105
+ functions,
106
+ companyDomain,
107
+ keywords,
108
+ limit,
109
+ pageToken,
110
+ };
111
+ }
112
+
113
+ export function assertLeadsFindArgs(args: string[]): void {
114
+ const copy = [...args];
115
+ hasFlag(copy, "--json");
116
+ const verb = copy[0];
117
+ if (verb !== "find") return;
118
+ parseLeadsFindFlags(copy.slice(1));
119
+ }
120
+
121
+ function csvFlag(args: string[], name: string): string[] | undefined {
122
+ const raw = getFlag(args, name);
123
+ if (!raw) return undefined;
124
+ return raw
125
+ .split(",")
126
+ .map((part) => part.trim())
127
+ .filter(Boolean);
128
+ }
129
+
130
+ function parseLimit(args: string[]): number | undefined {
131
+ const raw = getFlag(args, "--limit");
132
+ if (!raw) return undefined;
133
+ const parsed = Number(raw);
134
+ if (!Number.isInteger(parsed) || parsed < 1) {
135
+ throw new FindCliError("--limit must be a positive integer");
136
+ }
137
+ return Math.min(parsed, 5000);
138
+ }
139
+
140
+ function out(ctx: FindCliContext, value: unknown): void {
141
+ console.log(JSON.stringify(value, null, ctx.jsonOutput ? 2 : 0));
142
+ }
143
+
144
+ async function callQuery(
145
+ ctx: FindCliContext,
146
+ fn: Parameters<ConvexHttpClient["query"]>[0],
147
+ args: Record<string, unknown> = {},
148
+ ): Promise<unknown> {
149
+ return await ctx.convex.query(fn, {
150
+ ...args,
151
+ ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
152
+ } as never);
153
+ }
154
+
155
+ async function callAction(
156
+ ctx: FindCliContext,
157
+ fn: Parameters<ConvexHttpClient["action"]>[0],
158
+ args: Record<string, unknown> = {},
159
+ ): Promise<unknown> {
160
+ return await ctx.convex.action(fn, {
161
+ ...args,
162
+ ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
163
+ } as never);
164
+ }
165
+
166
+ function asArray(value: unknown): unknown[] {
167
+ return Array.isArray(value) ? value : [];
168
+ }
169
+
170
+ function findTitle(
171
+ kind: "people" | "companies" | "jobs",
172
+ parts: string[],
173
+ ): string {
174
+ const first = parts.find((part) => part.length > 0);
175
+ if (kind === "people") return first ? `People: ${first}` : "People search";
176
+ if (kind === "companies") {
177
+ return first ? `Companies: ${first}` : "Company search";
178
+ }
179
+ return first ? `Jobs: ${first}` : "Job search";
180
+ }
181
+
182
+ async function persist(
183
+ ctx: FindCliContext,
184
+ args: {
185
+ createdFrom: "find_people" | "find_companies" | "find_jobs";
186
+ title: string;
187
+ filters: Record<string, unknown>;
188
+ people?: unknown[];
189
+ companies?: unknown[];
190
+ jobs?: unknown[];
191
+ },
192
+ ): Promise<unknown> {
193
+ return await callAction(ctx, api.saveFind, args);
194
+ }
195
+
196
+ export function leadsHelp(): void {
197
+ console.log(`Dench leads
198
+
199
+ Usage:
200
+ dench leads find --titles "VP Sales" --locations "San Francisco" [--company-domain acme.com] [--limit 500] [--page-token <token>] [--json]
201
+ dench leads find --keywords "fintech" [--industries SaaS] [--seniorities director,vp] [--json]
202
+
203
+ Finds people through Dench search and writes a workspace artifact.
204
+ Import with \`dench artifacts import <id> --to lead\`.
205
+ `);
206
+ }
207
+
208
+ export function companiesHelp(): void {
209
+ console.log(`Dench companies
210
+
211
+ Usage:
212
+ dench companies find --name "Acme" [--domain acme.com] [--keywords fintech] [--industries SaaS] [--locations "San Francisco"] [--linkedin https://linkedin.com/company/acme] [--limit 500] [--page-token <token>] [--json]
213
+
214
+ Finds companies through Dench search and writes a workspace artifact.
215
+ Import with \`dench artifacts import <id> --to lead\`.
216
+ `);
217
+ }
218
+
219
+ export function jobsHelp(): void {
220
+ console.log(`Dench jobs
221
+
222
+ Usage:
223
+ dench jobs find --titles "Account Executive" --locations "San Francisco" [--company-domain acme.com] [--keywords remote] [--limit 500] [--page-token <token>] [--json]
224
+
225
+ Finds job postings through Dench search and writes a workspace artifact.
226
+ Import with \`dench artifacts import <id> --to job_posting\`.
227
+ `);
228
+ }
229
+
230
+ export function workspaceArtifactsHelp(): void {
231
+ console.log(`Dench artifacts
232
+
233
+ Usage:
234
+ dench artifacts [--limit 25] [--json]
235
+ dench artifacts list [--limit 25] [--json]
236
+ dench artifacts show <id> [--json]
237
+ dench artifacts import <id> --to lead|company|job_posting [--view "Q3 outbound"] [--pin] [--json]
238
+
239
+ \`list\` / \`show\` / \`import\` operate on find results (lead lists and job
240
+ searches). Bare \`dench artifacts\` still lists agent suggested-work artifacts.
241
+ `);
242
+ }
243
+
244
+ export function assertJobsFindArgs(args: string[]): void {
245
+ const copy = [...args];
246
+ hasFlag(copy, "--json");
247
+ if (copy[0] !== "find") return;
248
+ if (
249
+ !copy.includes("--titles") &&
250
+ !copy.includes("--locations") &&
251
+ !copy.includes("--company-domain") &&
252
+ !copy.includes("--company") &&
253
+ !copy.includes("--keywords")
254
+ ) {
255
+ throw new FindCliError(
256
+ "dench jobs find requires at least one of --titles, --locations, --company-domain, --company, --keywords",
257
+ );
258
+ }
259
+ }
260
+
261
+ export function assertCompaniesFindArgs(args: string[]): void {
262
+ const copy = [...args];
263
+ hasFlag(copy, "--json");
264
+ if (copy[0] !== "find") return;
265
+ if (
266
+ !copy.includes("--domain") &&
267
+ !copy.includes("--name") &&
268
+ !copy.includes("--keywords") &&
269
+ !copy.includes("--industries") &&
270
+ !copy.includes("--locations") &&
271
+ !copy.includes("--linkedin") &&
272
+ !copy.includes("--headcount")
273
+ ) {
274
+ throw new FindCliError(
275
+ "dench companies find requires at least one of --name, --domain, --keywords, --industries, --locations, --linkedin, --headcount",
276
+ );
277
+ }
278
+ }
279
+
280
+ export async function runLeadsCommand(opts: {
281
+ convex: ConvexHttpClient;
282
+ args: string[];
283
+ jsonOutput?: boolean;
284
+ sessionToken?: string;
285
+ enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
286
+ }): Promise<void> {
287
+ const args = [...opts.args];
288
+ const jsonFromArgs = hasFlag(args, "--json");
289
+ const ctx: FindCliContext = {
290
+ convex: opts.convex,
291
+ args,
292
+ jsonOutput: opts.jsonOutput === true || jsonFromArgs,
293
+ sessionToken: opts.sessionToken,
294
+ enrichmentGateway: opts.enrichmentGateway,
295
+ };
296
+ const verb = args.shift();
297
+ if (!verb || verb === "help" || verb === "--help") {
298
+ leadsHelp();
299
+ return;
300
+ }
301
+ if (verb !== "find") {
302
+ throw new FindCliError(`Unknown leads verb: ${verb}`);
303
+ }
304
+ const {
305
+ titles,
306
+ locations,
307
+ seniorities,
308
+ industries,
309
+ functions,
310
+ companyDomain,
311
+ keywords,
312
+ limit,
313
+ pageToken,
314
+ } = parseLeadsFindFlags(args);
315
+ try {
316
+ const result = await searchPeople(
317
+ {
318
+ personTitles: titles,
319
+ personLocations: locations,
320
+ personSeniorities: seniorities,
321
+ personFunctions: functions,
322
+ organizationDomains: companyDomain ? [companyDomain] : undefined,
323
+ organizationIndustries: industries,
324
+ keywords,
325
+ limit,
326
+ pageToken,
327
+ },
328
+ ctx.enrichmentGateway,
329
+ );
330
+ const people = asArray(result.people);
331
+ const artifact = await persist(ctx, {
332
+ createdFrom: "find_people",
333
+ title: findTitle("people", [
334
+ titles?.[0] ?? "",
335
+ keywords ?? "",
336
+ companyDomain ?? "",
337
+ ]),
338
+ filters: {
339
+ titles,
340
+ locations,
341
+ seniorities,
342
+ industries,
343
+ companyDomain,
344
+ keywords,
345
+ limit,
346
+ },
347
+ people,
348
+ });
349
+ // metadata carries total / nextPageToken — pass the token back via
350
+ // --page-token to continue where this page stopped.
351
+ out(ctx, {
352
+ ...(artifact as Record<string, unknown>),
353
+ metadata: result.metadata,
354
+ });
355
+ } catch (error) {
356
+ throw new FindCliError(
357
+ error instanceof EnrichmentGatewayError || error instanceof Error
358
+ ? error.message
359
+ : String(error),
360
+ );
361
+ }
362
+ }
363
+
364
+ export async function runJobsCommand(opts: {
365
+ convex: ConvexHttpClient;
366
+ args: string[];
367
+ jsonOutput?: boolean;
368
+ sessionToken?: string;
369
+ enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
370
+ }): Promise<void> {
371
+ const args = [...opts.args];
372
+ const jsonFromArgs = hasFlag(args, "--json");
373
+ const ctx: FindCliContext = {
374
+ convex: opts.convex,
375
+ args,
376
+ jsonOutput: opts.jsonOutput === true || jsonFromArgs,
377
+ sessionToken: opts.sessionToken,
378
+ enrichmentGateway: opts.enrichmentGateway,
379
+ };
380
+ const verb = args.shift();
381
+ if (!verb || verb === "help" || verb === "--help") {
382
+ jobsHelp();
383
+ return;
384
+ }
385
+ if (verb !== "find") {
386
+ throw new FindCliError(`Unknown jobs verb: ${verb}`);
387
+ }
388
+ const titles = csvFlag(args, "--titles");
389
+ const locations = csvFlag(args, "--locations");
390
+ const companyDomain = getFlag(args, "--company-domain");
391
+ const companyName = getFlag(args, "--company");
392
+ const keywords = getFlag(args, "--keywords");
393
+ const postedAfter = getFlag(args, "--posted-after");
394
+ const limit = parseLimit(args);
395
+ const pageToken = getFlag(args, "--page-token");
396
+ assertNoUnknownFlags(args, "jobs find");
397
+ if (!titles && !locations && !companyDomain && !companyName && !keywords) {
398
+ throw new FindCliError(
399
+ "dench jobs find requires at least one of --titles, --locations, --company-domain, --company, --keywords",
400
+ );
401
+ }
402
+ try {
403
+ const result = await searchJobs(
404
+ {
405
+ titles,
406
+ locations,
407
+ companyDomain,
408
+ companyName,
409
+ keywords,
410
+ postedAfter,
411
+ limit,
412
+ pageToken,
413
+ },
414
+ ctx.enrichmentGateway,
415
+ );
416
+ const jobs = asArray(result.jobs);
417
+ const artifact = await persist(ctx, {
418
+ createdFrom: "find_jobs",
419
+ title: findTitle("jobs", [titles?.[0] ?? "", companyName ?? "", keywords ?? ""]),
420
+ filters: {
421
+ titles,
422
+ locations,
423
+ companyDomain,
424
+ companyName,
425
+ keywords,
426
+ postedAfter,
427
+ limit,
428
+ },
429
+ jobs,
430
+ });
431
+ out(ctx, {
432
+ ...(artifact as Record<string, unknown>),
433
+ metadata: result.metadata,
434
+ });
435
+ } catch (error) {
436
+ throw new FindCliError(
437
+ error instanceof EnrichmentGatewayError || error instanceof Error
438
+ ? error.message
439
+ : String(error),
440
+ );
441
+ }
442
+ }
443
+
444
+ export async function runCompaniesCommand(opts: {
445
+ convex: ConvexHttpClient;
446
+ args: string[];
447
+ jsonOutput?: boolean;
448
+ sessionToken?: string;
449
+ enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
450
+ }): Promise<void> {
451
+ const args = [...opts.args];
452
+ const jsonFromArgs = hasFlag(args, "--json");
453
+ const ctx: FindCliContext = {
454
+ convex: opts.convex,
455
+ args,
456
+ jsonOutput: opts.jsonOutput === true || jsonFromArgs,
457
+ sessionToken: opts.sessionToken,
458
+ enrichmentGateway: opts.enrichmentGateway,
459
+ };
460
+ const verb = args.shift();
461
+ if (!verb || verb === "help" || verb === "--help") {
462
+ companiesHelp();
463
+ return;
464
+ }
465
+ if (verb !== "find") {
466
+ throw new FindCliError(`Unknown companies verb: ${verb}`);
467
+ }
468
+ const domain = getFlag(args, "--domain");
469
+ const name = getFlag(args, "--name");
470
+ const keywords = getFlag(args, "--keywords");
471
+ const industries = csvFlag(args, "--industries");
472
+ const locations = csvFlag(args, "--locations");
473
+ const linkedinUrl = getFlag(args, "--linkedin");
474
+ const headcountRanges = csvFlag(args, "--headcount");
475
+ const limit = parseLimit(args);
476
+ const pageToken = getFlag(args, "--page-token");
477
+ assertNoUnknownFlags(args, "companies find");
478
+ if (
479
+ !domain &&
480
+ !name &&
481
+ !keywords &&
482
+ !industries &&
483
+ !locations &&
484
+ !linkedinUrl &&
485
+ !headcountRanges
486
+ ) {
487
+ throw new FindCliError(
488
+ "dench companies find requires at least one of --name, --domain, --keywords, --industries, --locations, --linkedin, --headcount",
489
+ );
490
+ }
491
+ try {
492
+ const result = await searchCompanies(
493
+ {
494
+ domain,
495
+ name,
496
+ keywords,
497
+ industries,
498
+ locations,
499
+ linkedinUrl,
500
+ headcountRanges,
501
+ limit,
502
+ pageToken,
503
+ },
504
+ ctx.enrichmentGateway,
505
+ );
506
+ const artifact = await persist(ctx, {
507
+ createdFrom: "find_companies",
508
+ title: findTitle("companies", [name ?? "", domain ?? "", keywords ?? ""]),
509
+ filters: {
510
+ domain,
511
+ name,
512
+ keywords,
513
+ industries,
514
+ locations,
515
+ linkedinUrl,
516
+ headcountRanges,
517
+ limit,
518
+ },
519
+ companies: asArray(result.companies),
520
+ });
521
+ out(ctx, {
522
+ ...(artifact as Record<string, unknown>),
523
+ metadata: result.metadata,
524
+ });
525
+ } catch (error) {
526
+ throw new FindCliError(
527
+ error instanceof EnrichmentGatewayError || error instanceof Error
528
+ ? error.message
529
+ : String(error),
530
+ );
531
+ }
532
+ }
533
+
534
+ export async function runWorkspaceArtifactsCommand(opts: {
535
+ convex: ConvexHttpClient;
536
+ args: string[];
537
+ jsonOutput?: boolean;
538
+ sessionToken?: string;
539
+ }): Promise<boolean> {
540
+ const args = [...opts.args];
541
+ const verb = args[0];
542
+ if (verb !== "list" && verb !== "show" && verb !== "import") {
543
+ return false;
544
+ }
545
+ args.shift();
546
+ const ctx: FindCliContext = {
547
+ convex: opts.convex,
548
+ args,
549
+ jsonOutput: opts.jsonOutput === true || hasFlag(args, "--json"),
550
+ sessionToken: opts.sessionToken,
551
+ };
552
+ if (verb === "list") {
553
+ const limitRaw = getFlag(args, "--limit");
554
+ const limit = limitRaw ? Number(limitRaw) : 25;
555
+ assertNoUnknownFlags(args, "artifacts list");
556
+ out(ctx, await callQuery(ctx, api.list, { limit }));
557
+ return true;
558
+ }
559
+ if (verb === "show") {
560
+ const artifactId = shift(args, "artifact id");
561
+ assertNoUnknownFlags(args, "artifacts show");
562
+ out(ctx, await callQuery(ctx, api.get, { artifactId }));
563
+ return true;
564
+ }
565
+ const artifactId = shift(args, "artifact id");
566
+ const to = getFlag(args, "--to");
567
+ if (to !== "lead" && to !== "job_posting" && to !== "company") {
568
+ throw new FindCliError(
569
+ "dench artifacts import <id> --to lead|company|job_posting [--view ...] [--pin]",
570
+ );
571
+ }
572
+ const viewName = getFlag(args, "--view");
573
+ const pin = hasFlag(args, "--pin");
574
+ assertNoUnknownFlags(args, "artifacts import");
575
+ out(
576
+ ctx,
577
+ await callAction(ctx, api.importArtifact, {
578
+ artifactId,
579
+ to,
580
+ viewName,
581
+ pin,
582
+ }),
583
+ );
584
+ return true;
585
+ }
@@ -949,8 +949,23 @@ const toolRunResponse = z
949
949
 
950
950
  const enrichmentMetadata = z
951
951
  .object({
952
- total: z.number().optional(),
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();
@@ -1189,6 +1204,10 @@ const peopleSearch = z.object({
1189
1204
  exactMatch: z.boolean().optional(),
1190
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
 
@@ -1199,6 +1218,10 @@ const companySearch = z.object({
1199
1218
  exactMatch: z.boolean().optional(),
1200
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
 
@@ -1211,6 +1234,10 @@ const jobsSearch = z.object({
1211
1234
  keywords: z.string().optional(),
1212
1235
  postedAfter: z.string().optional(),
1213
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."),
1214
1241
  preferCache: z.boolean().optional(),
1215
1242
  });
1216
1243
 
@@ -1231,6 +1258,7 @@ const jobsSearchResponse = z
1231
1258
  .passthrough(),
1232
1259
  )
1233
1260
  .describe("Job postings in the canonical job_posting shape."),
1261
+ metadata: enrichmentMetadata,
1234
1262
  })
1235
1263
  .passthrough();
1236
1264
 
@@ -91,6 +91,22 @@ export const PEOPLE_ENRICHMENT_COLUMNS: readonly EnrichmentColumnDef[] = [
91
91
  extractionFallbacks: ["email"],
92
92
  enrichFields: ["work_emails"],
93
93
  },
94
+ {
95
+ label: "Work Email",
96
+ key: "person.work_email",
97
+ fieldType: "email",
98
+ apolloPath: "person.work_email",
99
+ requiredFields: ["work_email"],
100
+ enrichFields: ["work_emails"],
101
+ },
102
+ {
103
+ label: "Personal Email",
104
+ key: "person.personal_email",
105
+ fieldType: "email",
106
+ apolloPath: "person.personal_email",
107
+ requiredFields: ["personal_email"],
108
+ enrichFields: ["personal_emails"],
109
+ },
94
110
  {
95
111
  label: "Headline",
96
112
  key: "person.headline",
@@ -336,7 +352,14 @@ export function getRequiredFieldsForApolloPath(
336
352
  const column = getEnrichmentColumns(category).find(
337
353
  (candidate) => candidate.apolloPath === apolloPath,
338
354
  );
339
- return column?.requiredFields ?? [];
355
+ if (column) return column.requiredFields;
356
+ if (category !== "people") return [];
357
+ if (apolloPath.includes("personal_email")) return ["personal_email"];
358
+ if (apolloPath.includes("phone")) return ["phone"];
359
+ if (apolloPath.includes("work_email") || /(^|\.)email$/.test(apolloPath)) {
360
+ return ["work_email"];
361
+ }
362
+ return [];
340
363
  }
341
364
 
342
365
  export function buildEnrichmentMeta(
@@ -69,6 +69,8 @@ export type SearchPeopleBody = {
69
69
  limit?: number;
70
70
  offset?: number;
71
71
  searchAfter?: unknown;
72
+ /** Opaque token from a previous response's `metadata.nextPageToken`. */
73
+ pageToken?: string;
72
74
  // Rich filters, gateway-named. Merged with the legacy person*/organization*
73
75
  // aliases above; entries may carry per-value exclude / exactMatch.
74
76
  titles?: SearchFilterItem[];
@@ -104,6 +106,8 @@ export type SearchCompaniesBody = {
104
106
  keywords?: string;
105
107
  limit?: number;
106
108
  offset?: number;
109
+ /** Opaque token from a previous response's `metadata.nextPageToken`. */
110
+ pageToken?: string;
107
111
  };
108
112
 
109
113
  export type SearchJobsBody = {
@@ -115,6 +119,8 @@ export type SearchJobsBody = {
115
119
  keywords?: string;
116
120
  postedAfter?: string;
117
121
  limit?: number;
122
+ /** Opaque token from a previous response's `metadata.nextPageToken`. */
123
+ pageToken?: string;
118
124
  };
119
125
 
120
126
  export type EnrichmentGatewayOptions = {
@@ -1315,6 +1321,7 @@ export async function searchPeople(
1315
1321
  else if (body.perPage !== undefined) payload.limit = body.perPage;
1316
1322
  if (body.offset !== undefined) payload.offset = body.offset;
1317
1323
  if (body.searchAfter !== undefined) payload.searchAfter = body.searchAfter;
1324
+ if (body.pageToken) payload.pageToken = body.pageToken;
1318
1325
 
1319
1326
  return callGatewayJson("/v1/enrichment/people/search", {
1320
1327
  method: "POST",
@@ -1338,6 +1345,7 @@ export async function searchCompanies(
1338
1345
  if (body.keywords) payload.keywords = body.keywords;
1339
1346
  if (body.limit !== undefined) payload.limit = body.limit;
1340
1347
  if (body.offset !== undefined) payload.offset = body.offset;
1348
+ if (body.pageToken) payload.pageToken = body.pageToken;
1341
1349
  return callGatewayJson("/v1/enrichment/company/search", {
1342
1350
  method: "POST",
1343
1351
  body: payload,
@@ -1359,6 +1367,7 @@ export async function searchJobs(
1359
1367
  if (body.keywords) payload.keywords = body.keywords;
1360
1368
  if (body.postedAfter) payload.postedAfter = body.postedAfter;
1361
1369
  if (body.limit !== undefined) payload.limit = body.limit;
1370
+ if (body.pageToken) payload.pageToken = body.pageToken;
1362
1371
  return callGatewayJson("/v1/enrichment/jobs/search", {
1363
1372
  method: "POST",
1364
1373
  body: payload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.8.0-staging.107",
3
+ "version": "2.8.2-staging.112",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "search.ts",
33
33
  "web.ts",
34
34
  "image.ts",
35
+ "leads.ts",
35
36
  "tools.ts",
36
37
  "apps.ts",
37
38
  "agentKind.ts",
package/tools.ts CHANGED
@@ -169,7 +169,9 @@ function withoutShadowedComposioTools(
169
169
  * whole command over that would take `dench integrations` from "missing the
170
170
  * calendar" to "broken", which is strictly worse.
171
171
  */
172
- async function fetchUnipileConnections(ctx: UnipileAuthContext): Promise<{
172
+ export async function fetchUnipileConnections(
173
+ ctx: UnipileAuthContext,
174
+ ): Promise<{
173
175
  available: boolean;
174
176
  connections: CompactConnection[];
175
177
  error?: string;
@@ -257,12 +259,9 @@ async function runStatusSubcommand(ctx: ToolCliContext): Promise<void> {
257
259
  const filterToolkit = ctx.args[0]?.startsWith("--") ? undefined : ctx.args[0];
258
260
  if (filterToolkit) ctx.args.shift();
259
261
 
260
- const [connectionsPayload, unipile] = await Promise.all([
261
- callGateway(ctx, "GET", "/v1/composio/connections"),
262
- fetchUnipileConnections(ctx),
263
- ]);
262
+ const connectionsSummary = await fetchConnectedAppsSummary(ctx);
264
263
  let toolkitsPayload: unknown = null;
265
- if (filterToolkit) {
264
+ if (filterToolkit && connectionsSummary.available) {
266
265
  const search = encodeURIComponent(filterToolkit);
267
266
  toolkitsPayload = await callGateway(
268
267
  ctx,
@@ -271,10 +270,7 @@ async function runStatusSubcommand(ctx: ToolCliContext): Promise<void> {
271
270
  );
272
271
  }
273
272
 
274
- const allConnections = [
275
- ...extractConnections(connectionsPayload).map(compactConnection),
276
- ...unipile.connections,
277
- ];
273
+ const allConnections = connectionsSummary.connections;
278
274
  // Filtering after the merge is what makes `dench tool status gmail` answer
279
275
  // with the Unipile mailbox instead of nothing.
280
276
  const filteredConnections = filterToolkit
@@ -285,12 +281,16 @@ async function runStatusSubcommand(ctx: ToolCliContext): Promise<void> {
285
281
 
286
282
  const summary = {
287
283
  ok: true,
288
- available: true,
284
+ available: connectionsSummary.available,
289
285
  toolkit: filterToolkit ?? null,
290
286
  count: filteredConnections.length,
291
287
  connections: filteredConnections,
292
- unipileAvailable: unipile.available,
293
- ...(unipile.error ? { unipileError: unipile.error } : {}),
288
+ unipileAvailable: connectionsSummary.unipileAvailable,
289
+ ...(connectionsSummary.unipileError
290
+ ? { unipileError: connectionsSummary.unipileError }
291
+ : {}),
292
+ ...(connectionsSummary.error ? { error: connectionsSummary.error } : {}),
293
+ ...(connectionsSummary.reason ? { reason: connectionsSummary.reason } : {}),
294
294
  ...(toolkitsPayload
295
295
  ? { toolkit_info: extractToolkits(toolkitsPayload) }
296
296
  : {}),