@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/agent-config.ts +645 -0
- package/crm.ts +106 -8
- package/dench.ts +47 -0
- package/lib/enrichment-gateway.ts +1063 -42
- package/members.ts +192 -0
- package/package.json +3 -1
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
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
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
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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;
|