@dench.com/cli 0.4.7 → 0.4.9

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
@@ -48,11 +48,21 @@ import {
48
48
  type FieldCandidate,
49
49
  } from "./lib/crm-enrichment";
50
50
  import {
51
+ aviatoEnrichCompany,
52
+ aviatoEnrichPerson,
51
53
  enrichCompany,
52
54
  EnrichmentGatewayError,
53
55
  enrichPersonByIdentifier,
54
56
  } from "./lib/enrichment-gateway";
55
57
 
58
+ // Contact-info fields that Aviato does not return.
59
+ const CONTACT_REQUIRED_FIELDS = ["phone", "email", "personal_email"] as const;
60
+ type ContactRequiredField = (typeof CONTACT_REQUIRED_FIELDS)[number];
61
+
62
+ function isContactField(field: string): field is ContactRequiredField {
63
+ return (CONTACT_REQUIRED_FIELDS as readonly string[]).includes(field);
64
+ }
65
+
56
66
  type JsonRecord = Record<string, unknown>;
57
67
 
58
68
  const CRM_BATCH_CHUNK_SIZE = 200;
@@ -412,6 +422,9 @@ const api = {
412
422
  "functions/crm/actions:startActionRun",
413
423
  ),
414
424
  },
425
+ members: {
426
+ list: makeFunctionReference<"query">("functions/crm/members:list"),
427
+ },
415
428
  };
416
429
 
417
430
  export async function runCrmCommand(opts: {
@@ -472,11 +485,31 @@ export async function runCrmCommand(opts: {
472
485
  return await runDocsCommand(ctx);
473
486
  case "actions":
474
487
  return await runActionsCommand(ctx);
488
+ case "members":
489
+ // Alias for `dench members` so the AI agent finds the roster
490
+ // surface from inside the `dench crm` namespace it's primed
491
+ // on. Delegates to the shared implementation in cli/members.ts
492
+ // so payload formatting + flag handling stay in lockstep.
493
+ return await runCrmMembersCommand(ctx);
475
494
  default:
476
495
  throw new CrmCliError(`Unknown crm subcommand: ${subcommand}`);
477
496
  }
478
497
  }
479
498
 
499
+ async function runCrmMembersCommand(ctx: CrmCliContext): Promise<void> {
500
+ const { runMembersCommand } = await import("./members");
501
+ // Re-prepend `--json` so the members dispatcher's own `hasFlag`
502
+ // pass sees it again — we already drained it once when computing
503
+ // `ctx.jsonOutput`, but the members CLI re-parses argv from scratch
504
+ // (same shape as a top-level invocation).
505
+ const args = ctx.jsonOutput ? ["--json", ...ctx.args] : [...ctx.args];
506
+ await runMembersCommand({
507
+ convex: ctx.convex,
508
+ args,
509
+ sessionToken: ctx.sessionToken,
510
+ });
511
+ }
512
+
480
513
  async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
481
514
  const verb = ctx.args.shift();
482
515
  switch (verb) {
@@ -665,8 +698,32 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
665
698
  case "list": {
666
699
  const objectName = shift(ctx.args, "object name");
667
700
  const limit = parseInt(getFlag(ctx.args, "--limit") ?? "100", 10);
701
+ const withMeta = hasFlag(ctx.args, "--with-meta");
668
702
  assertNoUnknownFlags(ctx.args, "crm entries list");
669
- out(ctx, await callQuery(ctx, api.entries.list, { objectName, limit }));
703
+ const entries = await callQuery(ctx, api.entries.list, {
704
+ objectName,
705
+ limit,
706
+ });
707
+ if (!withMeta) {
708
+ out(ctx, entries);
709
+ return;
710
+ }
711
+ const object = (await callQuery(ctx, api.objects.get, {
712
+ name: objectName,
713
+ })) as { entryCount?: number } | null;
714
+ const returnedCount = Array.isArray(entries) ? entries.length : 0;
715
+ const totalCount =
716
+ typeof object?.entryCount === "number"
717
+ ? object.entryCount
718
+ : returnedCount;
719
+ out(ctx, {
720
+ objectName,
721
+ totalCount,
722
+ returnedCount,
723
+ limit,
724
+ hasMore: totalCount > returnedCount,
725
+ entries,
726
+ });
670
727
  return;
671
728
  }
672
729
  case "get": {
@@ -1634,6 +1691,8 @@ async function resolveEnrichmentTarget(
1634
1691
  async function callEnrichmentGatewayForEntry(args: {
1635
1692
  target: EnrichmentTarget;
1636
1693
  entry: CrmEntryForEnrichment;
1694
+ /** Explicit provider override. Defaults: people=fullenrich, company=aviato. */
1695
+ provider?: string;
1637
1696
  }): Promise<Record<string, unknown>> {
1638
1697
  const input = args.entry.fields[args.target.inputField.name];
1639
1698
  if (!hasCellValue(input)) {
@@ -1642,13 +1701,42 @@ async function callEnrichmentGatewayForEntry(args: {
1642
1701
  );
1643
1702
  }
1644
1703
  const inputValue = String(input);
1645
- if (args.target.category === "people") {
1646
- return await enrichPersonByIdentifier(inputValue, args.target.requiredFields);
1704
+ const isCompany = args.target.category === "company";
1705
+ const resolvedProvider = args.provider ?? (isCompany ? "aviato" : "fullenrich");
1706
+
1707
+ if (resolvedProvider === "aviato") {
1708
+ // Guard: Aviato does not return phone or email.
1709
+ const contactFields = args.target.requiredFields.filter(isContactField);
1710
+ if (contactFields.length > 0) {
1711
+ throw new CrmCliError(
1712
+ `Aviato does not return phone or email; use --provider fullenrich for contact fields (${contactFields.join(", ")}).`,
1713
+ );
1714
+ }
1715
+
1716
+ if (isCompany) {
1717
+ // inputValue is typically a domain; try to detect linkedin URLs
1718
+ const isLinkedin = /linkedin\.com/i.test(inputValue);
1719
+ return aviatoEnrichCompany(
1720
+ isLinkedin ? { linkedinUrl: inputValue } : { website: inputValue },
1721
+ );
1722
+ } else {
1723
+ // For people, inputValue may be a LinkedIn URL, email, or LinkedIn ID
1724
+ const isLinkedin = /linkedin\.com/i.test(inputValue);
1725
+ const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inputValue);
1726
+ if (isLinkedin) return aviatoEnrichPerson({ linkedinUrl: inputValue });
1727
+ if (isEmail) return aviatoEnrichPerson({ email: inputValue });
1728
+ return aviatoEnrichPerson({ linkedinID: inputValue });
1729
+ }
1647
1730
  }
1648
- return await enrichCompany({
1649
- domain: inputValue,
1650
- requiredFields: args.target.requiredFields,
1651
- });
1731
+
1732
+ // fullenrich path (default for people, explicit override for companies)
1733
+ if (isCompany) {
1734
+ return enrichCompany({
1735
+ domain: inputValue,
1736
+ requiredFields: args.target.requiredFields,
1737
+ });
1738
+ }
1739
+ return enrichPersonByIdentifier(inputValue, args.target.requiredFields);
1652
1740
  }
1653
1741
 
1654
1742
  async function enrichOneEntry(
@@ -1662,6 +1750,7 @@ async function enrichOneEntry(
1662
1750
  const payload = await callEnrichmentGatewayForEntry({
1663
1751
  target: args.target,
1664
1752
  entry: args.entry,
1753
+ provider: args.provider,
1665
1754
  });
1666
1755
  const value = extractEnrichmentValue(payload, args.target.column);
1667
1756
  if (value === null) {
@@ -2026,7 +2115,7 @@ Fields (columns):
2026
2115
  dench crm fields reorder <object> <field1> <field2> <field3> ...
2027
2116
 
2028
2117
  Entries (rows):
2029
- dench crm entries list <object> [--limit N] [--json]
2118
+ dench crm entries list <object> [--limit N] [--with-meta] [--json]
2030
2119
  dench crm entries get <object> <entryId>
2031
2120
  dench crm entries create <object> --data '{"Field":"Value",...}' (alias: --fields)
2032
2121
  dench crm entries create-many <object> --data '[{"Field":"Value",...}]' [--file rows.json] [--idempotency-key <key>]
@@ -2057,6 +2146,15 @@ Statuses (kanban columns):
2057
2146
  dench crm statuses list <object>
2058
2147
  dench crm statuses set <object> --statuses '[{"name":"New","color":"#94a3b8"},...]'
2059
2148
 
2149
+ Workspace members (for user-type fields):
2150
+ dench crm members list [--json] (alias: dench members list)
2151
+ List \`{ userId, role, name, email }\` for every member of the
2152
+ current org. Use the userIds when setting \`Owner\`, \`Assignee\`,
2153
+ or any other \`user\`-type field so the UI shows a real linked
2154
+ avatar instead of an unlinked literal-string badge:
2155
+ dench crm cells set task --entry <id> --field Owner --value <userId>
2156
+ dench crm entries create task --data '{"Name":"X","Owner":"<userId>"}'
2157
+
2060
2158
  Batch:
2061
2159
  dench crm batch --file ops.jsonl [--idempotency-key <key>]
2062
2160
  For repeated writes, prefer entries create-many/update-many or cells set-many;
package/dench.ts CHANGED
@@ -478,6 +478,13 @@ CRM (Daytona-native rewrite):
478
478
  Batch write helpers: entries create-many/update-many, cells set-many
479
479
  Help: dench crm help
480
480
 
481
+ Workspace members (for CRM \`user\`-type fields):
482
+ dench members list [--json] (top-level alias)
483
+ dench crm members list [--json] (canonical CRM-scoped name)
484
+ Look up real userIds for the active org so \`Owner\` / \`Assignee\`
485
+ cells assign to real members instead of being stored as unlinked
486
+ display-name strings. See \`dench members help\`.
487
+
481
488
  Live web search (Exa via the Dench Cloud Gateway, auths with DENCH_API_KEY):
482
489
  dench search "<query>" [--num-results N] [--type auto|fast|deep|deep-reasoning|neural] [--category news|company|people|...] [--include-domains a.com,b.com] [--exclude-domains c.com] [--max-chars 800] [--json]
483
490
  dench search contents <url> [<url>...] [--max-chars 4000] [--summary "..."] [--json]
@@ -3272,6 +3279,46 @@ async function main() {
3272
3279
  return;
3273
3280
  }
3274
3281
 
3282
+ if (command === "members") {
3283
+ // Top-level convenience surface for the same Convex query that
3284
+ // `dench crm members list` calls. Lives here (rather than only
3285
+ // under `dench crm`) so it shows up in top-level help and tab
3286
+ // completion — members aren't strictly a CRM concept, even though
3287
+ // the canonical caller today is the AI agent looking up userIds
3288
+ // for `user`-typed CRM cells.
3289
+ const subArgs = stripRuntimeFlags(args.slice(args.indexOf("members") + 1));
3290
+ const sub = subArgs[0];
3291
+ if (!sub || sub === "help" || sub === "--help") {
3292
+ const { runMembersCommand } = await import("./members");
3293
+ // biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
3294
+ await runMembersCommand({ convex: {} as any, args: subArgs });
3295
+ return;
3296
+ }
3297
+ const { runMembersCommand } = await import("./members");
3298
+ const runtime = await getRuntime();
3299
+ // Member listing is org-scoped via `requireCrmAccess`, identical
3300
+ // auth shape to `dench crm`. Dev workspaces forward the encoded
3301
+ // dev token so local --dev runs work the same way.
3302
+ if (runtime.mode === "dev") {
3303
+ await runMembersCommand({
3304
+ convex: runtime.client,
3305
+ args: subArgs,
3306
+ sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
3307
+ });
3308
+ return;
3309
+ }
3310
+ const authedRuntime = requireAuthenticatedRuntime(
3311
+ runtime,
3312
+ "dench members",
3313
+ );
3314
+ await runMembersCommand({
3315
+ convex: authedRuntime.client,
3316
+ args: subArgs,
3317
+ sessionToken: authedRuntime.sessionToken,
3318
+ });
3319
+ return;
3320
+ }
3321
+
3275
3322
  if (command === "fs") {
3276
3323
  await runFsCommand();
3277
3324
  return;