@dench.com/cli 2.0.4 → 2.1.1
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 +368 -65
- package/dench.ts +39 -3
- package/lib/enrichment-gateway.ts +22 -0
- package/package.json +1 -1
- package/tools.ts +3 -1
package/crm.ts
CHANGED
|
@@ -48,12 +48,16 @@ import {
|
|
|
48
48
|
getRequiredFieldsForApolloPath,
|
|
49
49
|
} from "./lib/crm-enrichment";
|
|
50
50
|
import {
|
|
51
|
+
type AviatoEnrichCompanyBody,
|
|
52
|
+
type AviatoEnrichPersonBody,
|
|
51
53
|
aviatoEnrichCompany,
|
|
52
54
|
aviatoEnrichPerson,
|
|
55
|
+
type EnrichCompanyBody,
|
|
53
56
|
EnrichmentGatewayError,
|
|
54
57
|
type EnrichmentGatewayOptions,
|
|
58
|
+
type EnrichPersonBody,
|
|
55
59
|
enrichCompany,
|
|
56
|
-
|
|
60
|
+
enrichPerson,
|
|
57
61
|
} from "./lib/enrichment-gateway";
|
|
58
62
|
|
|
59
63
|
// Contact-info fields that Aviato does not return.
|
|
@@ -90,6 +94,32 @@ type CrmCliContext = {
|
|
|
90
94
|
// don't need to change their try/catch shape.
|
|
91
95
|
class CrmCliError extends Error {}
|
|
92
96
|
|
|
97
|
+
function userFacingConvexErrorMessage(error: unknown): string | null {
|
|
98
|
+
if (!error || typeof error !== "object") return null;
|
|
99
|
+
const data = (error as { data?: unknown }).data;
|
|
100
|
+
if (typeof data === "string" && data.trim().length > 0) {
|
|
101
|
+
return data.trim();
|
|
102
|
+
}
|
|
103
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
104
|
+
const record = data as Record<string, unknown>;
|
|
105
|
+
for (const key of ["message", "error", "reason"]) {
|
|
106
|
+
const value = record[key];
|
|
107
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
108
|
+
return value.trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeRpcError(error: unknown): Error {
|
|
116
|
+
if (error instanceof CrmCliError) return error;
|
|
117
|
+
return new CrmCliError(
|
|
118
|
+
userFacingConvexErrorMessage(error) ??
|
|
119
|
+
(error instanceof Error ? error.message : String(error)),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
function shift(args: string[], expected: string): string {
|
|
94
124
|
try {
|
|
95
125
|
return shiftRaw(args, expected);
|
|
@@ -275,10 +305,14 @@ async function callQuery(
|
|
|
275
305
|
fn: Parameters<ConvexHttpClient["query"]>[0],
|
|
276
306
|
args: Record<string, unknown> = {},
|
|
277
307
|
): Promise<unknown> {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
308
|
+
try {
|
|
309
|
+
return await ctx.convex.query(fn, {
|
|
310
|
+
...args,
|
|
311
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
312
|
+
} as never);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw normalizeRpcError(error);
|
|
315
|
+
}
|
|
282
316
|
}
|
|
283
317
|
|
|
284
318
|
async function callMutation(
|
|
@@ -286,10 +320,14 @@ async function callMutation(
|
|
|
286
320
|
fn: Parameters<ConvexHttpClient["mutation"]>[0],
|
|
287
321
|
args: Record<string, unknown> = {},
|
|
288
322
|
): Promise<unknown> {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
323
|
+
try {
|
|
324
|
+
return await ctx.convex.mutation(fn, {
|
|
325
|
+
...args,
|
|
326
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
327
|
+
} as never);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw normalizeRpcError(error);
|
|
330
|
+
}
|
|
293
331
|
}
|
|
294
332
|
|
|
295
333
|
function out(ctx: CrmCliContext, value: unknown): void {
|
|
@@ -383,6 +421,21 @@ const api = {
|
|
|
383
421
|
update: makeFunctionReference<"mutation">("functions/crm/fields:update"),
|
|
384
422
|
remove: makeFunctionReference<"mutation">("functions/crm/fields:remove"),
|
|
385
423
|
reorder: makeFunctionReference<"mutation">("functions/crm/fields:reorder"),
|
|
424
|
+
reorderEnumValues: makeFunctionReference<"mutation">(
|
|
425
|
+
"functions/crm/fields:reorderEnumValues",
|
|
426
|
+
),
|
|
427
|
+
addEnumValue: makeFunctionReference<"mutation">(
|
|
428
|
+
"functions/crm/fields:addEnumValue",
|
|
429
|
+
),
|
|
430
|
+
removeEnumValue: makeFunctionReference<"mutation">(
|
|
431
|
+
"functions/crm/fields:removeEnumValue",
|
|
432
|
+
),
|
|
433
|
+
setEnumColor: makeFunctionReference<"mutation">(
|
|
434
|
+
"functions/crm/fields:setEnumColor",
|
|
435
|
+
),
|
|
436
|
+
renameEnumValue: makeFunctionReference<"mutation">(
|
|
437
|
+
"functions/crm/fields:renameEnumValue",
|
|
438
|
+
),
|
|
386
439
|
},
|
|
387
440
|
entries: {
|
|
388
441
|
list: makeFunctionReference<"query">("functions/crm/entries:list"),
|
|
@@ -693,6 +746,107 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
693
746
|
);
|
|
694
747
|
return;
|
|
695
748
|
}
|
|
749
|
+
case "enum-reorder": {
|
|
750
|
+
const objectName = shift(ctx.args, "object name");
|
|
751
|
+
const fieldName = shift(ctx.args, "field name");
|
|
752
|
+
// Same pattern as `reorder`: peel positional args after asserting
|
|
753
|
+
// there are no stray flags so a typo'd `--limit` fails loud
|
|
754
|
+
// instead of being silently consumed as an enum value.
|
|
755
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-reorder");
|
|
756
|
+
const orderedValues = [...ctx.args];
|
|
757
|
+
if (orderedValues.length === 0) {
|
|
758
|
+
throw new CrmCliError(
|
|
759
|
+
"dench crm fields enum-reorder requires at least one value. " +
|
|
760
|
+
'Pass each enum value as a positional arg: `enum-reorder task Status "Done" "In Progress" "To Do"`',
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
out(
|
|
764
|
+
ctx,
|
|
765
|
+
await callMutation(ctx, api.fields.reorderEnumValues, {
|
|
766
|
+
objectName,
|
|
767
|
+
fieldName,
|
|
768
|
+
orderedValues,
|
|
769
|
+
}),
|
|
770
|
+
);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
case "enum-add": {
|
|
774
|
+
const objectName = shift(ctx.args, "object name");
|
|
775
|
+
const fieldName = shift(ctx.args, "field name");
|
|
776
|
+
const value = shift(ctx.args, "enum value");
|
|
777
|
+
const color = getFlag(ctx.args, "--color");
|
|
778
|
+
const positionStr = getFlag(ctx.args, "--position");
|
|
779
|
+
const position =
|
|
780
|
+
positionStr !== undefined
|
|
781
|
+
? Number.parseInt(positionStr, 10)
|
|
782
|
+
: undefined;
|
|
783
|
+
if (position !== undefined && !Number.isFinite(position)) {
|
|
784
|
+
throw new CrmCliError(
|
|
785
|
+
`--position must be an integer, got "${positionStr}"`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-add");
|
|
789
|
+
out(
|
|
790
|
+
ctx,
|
|
791
|
+
await callMutation(ctx, api.fields.addEnumValue, {
|
|
792
|
+
objectName,
|
|
793
|
+
fieldName,
|
|
794
|
+
value,
|
|
795
|
+
color,
|
|
796
|
+
position,
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
case "enum-remove": {
|
|
802
|
+
const objectName = shift(ctx.args, "object name");
|
|
803
|
+
const fieldName = shift(ctx.args, "field name");
|
|
804
|
+
const value = shift(ctx.args, "enum value");
|
|
805
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-remove");
|
|
806
|
+
out(
|
|
807
|
+
ctx,
|
|
808
|
+
await callMutation(ctx, api.fields.removeEnumValue, {
|
|
809
|
+
objectName,
|
|
810
|
+
fieldName,
|
|
811
|
+
value,
|
|
812
|
+
}),
|
|
813
|
+
);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
case "enum-rename": {
|
|
817
|
+
const objectName = shift(ctx.args, "object name");
|
|
818
|
+
const fieldName = shift(ctx.args, "field name");
|
|
819
|
+
const oldValue = shift(ctx.args, "old enum value");
|
|
820
|
+
const newValue = shift(ctx.args, "new enum value");
|
|
821
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-rename");
|
|
822
|
+
out(
|
|
823
|
+
ctx,
|
|
824
|
+
await callMutation(ctx, api.fields.renameEnumValue, {
|
|
825
|
+
objectName,
|
|
826
|
+
fieldName,
|
|
827
|
+
oldValue,
|
|
828
|
+
newValue,
|
|
829
|
+
}),
|
|
830
|
+
);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
case "enum-set-color": {
|
|
834
|
+
const objectName = shift(ctx.args, "object name");
|
|
835
|
+
const fieldName = shift(ctx.args, "field name");
|
|
836
|
+
const value = shift(ctx.args, "enum value");
|
|
837
|
+
const color = shift(ctx.args, "hex color");
|
|
838
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-set-color");
|
|
839
|
+
out(
|
|
840
|
+
ctx,
|
|
841
|
+
await callMutation(ctx, api.fields.setEnumColor, {
|
|
842
|
+
objectName,
|
|
843
|
+
fieldName,
|
|
844
|
+
value,
|
|
845
|
+
color,
|
|
846
|
+
}),
|
|
847
|
+
);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
696
850
|
default:
|
|
697
851
|
throw new CrmCliError(`Unknown crm fields verb: ${verb ?? "<none>"}`);
|
|
698
852
|
}
|
|
@@ -1704,6 +1858,21 @@ async function resolveEnrichmentTarget(
|
|
|
1704
1858
|
};
|
|
1705
1859
|
}
|
|
1706
1860
|
|
|
1861
|
+
/**
|
|
1862
|
+
* Build the gateway request body from a CRM entry the same way the
|
|
1863
|
+
* chat-turn `enrich_person` / `enrich_company` / `enrich_person_basic` /
|
|
1864
|
+
* `enrich_company_basic` tools do — pass every structured identifier the
|
|
1865
|
+
* row has (email, firstName, lastName, linkedinUrl, domain, …) and let the
|
|
1866
|
+
* shared gateway client (`enrichPerson`, `enrichCompany`,
|
|
1867
|
+
* `aviatoEnrichPerson`, `aviatoEnrichCompany`) validate + dispatch.
|
|
1868
|
+
*
|
|
1869
|
+
* The previous implementation collapsed the entry to a single
|
|
1870
|
+
* `--input-field` value and called `enrichPersonByIdentifier`, which
|
|
1871
|
+
* threw away the rest of the row and made FullEnrich fail with
|
|
1872
|
+
* `missing_people_identifier` whenever the input field was just an
|
|
1873
|
+
* Email (no first/last name reached the gateway, so the validator
|
|
1874
|
+
* rejected the request before any provider was called).
|
|
1875
|
+
*/
|
|
1707
1876
|
async function callEnrichmentGatewayForEntry(args: {
|
|
1708
1877
|
target: EnrichmentTarget;
|
|
1709
1878
|
entry: CrmEntryForEnrichment;
|
|
@@ -1711,64 +1880,175 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1711
1880
|
provider?: string;
|
|
1712
1881
|
gateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
1713
1882
|
}): Promise<Record<string, unknown>> {
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1883
|
+
const { entry, target } = args;
|
|
1884
|
+
// Sanity gate: the column the user picked as `--input-field` must be
|
|
1885
|
+
// populated. The body builder will pass every other available identifier
|
|
1886
|
+
// too, so the gateway has the best possible chance of resolving the row.
|
|
1887
|
+
if (!hasCellValue(entry.fields[target.inputField.name])) {
|
|
1716
1888
|
throw new CrmCliError(
|
|
1717
|
-
`Entry ${
|
|
1889
|
+
`Entry ${entry.id} has no value for input field "${target.inputField.name}"`,
|
|
1718
1890
|
);
|
|
1719
1891
|
}
|
|
1720
|
-
const
|
|
1721
|
-
const isCompany = args.target.category === "company";
|
|
1892
|
+
const isCompany = target.category === "company";
|
|
1722
1893
|
const resolvedProvider =
|
|
1723
1894
|
args.provider ?? (isCompany ? "aviato" : "fullenrich");
|
|
1724
1895
|
|
|
1725
1896
|
if (resolvedProvider === "aviato") {
|
|
1726
|
-
//
|
|
1727
|
-
|
|
1897
|
+
// Aviato does not return phone or email — surface a clear error
|
|
1898
|
+
// before we waste a gateway round-trip.
|
|
1899
|
+
const contactFields = target.requiredFields.filter(isContactField);
|
|
1728
1900
|
if (contactFields.length > 0) {
|
|
1729
1901
|
throw new CrmCliError(
|
|
1730
1902
|
`Aviato does not return phone or email; use --provider fullenrich for contact fields (${contactFields.join(", ")}).`,
|
|
1731
1903
|
);
|
|
1732
1904
|
}
|
|
1733
|
-
|
|
1734
1905
|
if (isCompany) {
|
|
1735
|
-
// inputValue is typically a domain; try to detect linkedin URLs
|
|
1736
|
-
const isLinkedin = /linkedin\.com/i.test(inputValue);
|
|
1737
1906
|
return aviatoEnrichCompany(
|
|
1738
|
-
|
|
1907
|
+
buildAviatoCompanyBodyFromEntry(entry),
|
|
1739
1908
|
args.gateway,
|
|
1740
1909
|
);
|
|
1741
|
-
} else {
|
|
1742
|
-
// For people, inputValue may be a LinkedIn URL, email, or LinkedIn ID
|
|
1743
|
-
const isLinkedin = /linkedin\.com/i.test(inputValue);
|
|
1744
|
-
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inputValue);
|
|
1745
|
-
if (isLinkedin) {
|
|
1746
|
-
return aviatoEnrichPerson({ linkedinUrl: inputValue }, args.gateway);
|
|
1747
|
-
}
|
|
1748
|
-
if (isEmail) {
|
|
1749
|
-
return aviatoEnrichPerson({ email: inputValue }, args.gateway);
|
|
1750
|
-
}
|
|
1751
|
-
return aviatoEnrichPerson({ linkedinID: inputValue }, args.gateway);
|
|
1752
1910
|
}
|
|
1911
|
+
return aviatoEnrichPerson(
|
|
1912
|
+
buildAviatoPersonBodyFromEntry(entry),
|
|
1913
|
+
args.gateway,
|
|
1914
|
+
);
|
|
1753
1915
|
}
|
|
1754
1916
|
|
|
1755
|
-
//
|
|
1917
|
+
// FullEnrich path. Mirrors `enrichPersonStep` / `enrichCompanyStep` in
|
|
1918
|
+
// src/workflows/enrichment-steps.ts: build a structured body, hand it
|
|
1919
|
+
// to the same shared gateway client.
|
|
1756
1920
|
if (isCompany) {
|
|
1757
1921
|
return enrichCompany(
|
|
1758
|
-
|
|
1759
|
-
domain: inputValue,
|
|
1760
|
-
requiredFields: args.target.requiredFields,
|
|
1761
|
-
},
|
|
1922
|
+
buildCompanyBodyFromEntry(entry, target.requiredFields),
|
|
1762
1923
|
args.gateway,
|
|
1763
1924
|
);
|
|
1764
1925
|
}
|
|
1765
|
-
return
|
|
1766
|
-
|
|
1767
|
-
args.target.requiredFields,
|
|
1926
|
+
return enrichPerson(
|
|
1927
|
+
buildPersonBodyFromEntry(entry, target.requiredFields),
|
|
1768
1928
|
args.gateway,
|
|
1769
1929
|
);
|
|
1770
1930
|
}
|
|
1771
1931
|
|
|
1932
|
+
// ── Body builders ────────────────────────────────────────────────────────
|
|
1933
|
+
//
|
|
1934
|
+
// Pick whichever canonical column name the entry happens to use. Field
|
|
1935
|
+
// naming on CRM objects has historically drifted (`Name` vs `Full Name`,
|
|
1936
|
+
// `Email` vs `Email Address`, `Website` vs `Domain`), so each builder
|
|
1937
|
+
// accepts a small list of synonyms before giving up.
|
|
1938
|
+
|
|
1939
|
+
const PEOPLE_FULL_NAME_FIELDS = ["Name", "Full Name", "Person Name"] as const;
|
|
1940
|
+
const PEOPLE_FIRST_NAME_FIELDS = ["First Name", "Given Name"] as const;
|
|
1941
|
+
const PEOPLE_LAST_NAME_FIELDS = [
|
|
1942
|
+
"Last Name",
|
|
1943
|
+
"Family Name",
|
|
1944
|
+
"Surname",
|
|
1945
|
+
] as const;
|
|
1946
|
+
const PEOPLE_EMAIL_FIELDS = [
|
|
1947
|
+
"Email",
|
|
1948
|
+
"Email Address",
|
|
1949
|
+
"Work Email",
|
|
1950
|
+
"Personal Email",
|
|
1951
|
+
] as const;
|
|
1952
|
+
const PEOPLE_LINKEDIN_FIELDS = [
|
|
1953
|
+
"LinkedIn URL",
|
|
1954
|
+
"LinkedIn",
|
|
1955
|
+
"Linkedin URL",
|
|
1956
|
+
] as const;
|
|
1957
|
+
const PEOPLE_DOMAIN_FIELDS = ["Domain", "Company Domain"] as const;
|
|
1958
|
+
|
|
1959
|
+
const COMPANY_WEBSITE_FIELDS = ["Website", "Domain", "URL", "Site"] as const;
|
|
1960
|
+
const COMPANY_LINKEDIN_FIELDS = [
|
|
1961
|
+
"LinkedIn URL",
|
|
1962
|
+
"LinkedIn",
|
|
1963
|
+
"Linkedin URL",
|
|
1964
|
+
] as const;
|
|
1965
|
+
const COMPANY_NAME_FIELDS = [
|
|
1966
|
+
"Company Name",
|
|
1967
|
+
"Name",
|
|
1968
|
+
"Organization",
|
|
1969
|
+
"Organization Name",
|
|
1970
|
+
] as const;
|
|
1971
|
+
|
|
1972
|
+
function readEntryField(
|
|
1973
|
+
entry: CrmEntryForEnrichment,
|
|
1974
|
+
candidates: readonly string[],
|
|
1975
|
+
): string | undefined {
|
|
1976
|
+
for (const name of candidates) {
|
|
1977
|
+
const value = entry.fields[name];
|
|
1978
|
+
if (typeof value === "string") {
|
|
1979
|
+
const trimmed = value.trim();
|
|
1980
|
+
if (trimmed.length > 0) return trimmed;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return undefined;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
/** Split "Mendy Edri" → { firstName: "Mendy", lastName: "Edri" }. */
|
|
1987
|
+
export function splitFullName(fullName: string): {
|
|
1988
|
+
firstName?: string;
|
|
1989
|
+
lastName?: string;
|
|
1990
|
+
} {
|
|
1991
|
+
const trimmed = fullName.trim();
|
|
1992
|
+
if (!trimmed) return {};
|
|
1993
|
+
const parts = trimmed.split(/\s+/);
|
|
1994
|
+
if (parts.length === 1) return { firstName: parts[0] };
|
|
1995
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
export function buildPersonBodyFromEntry(
|
|
1999
|
+
entry: CrmEntryForEnrichment,
|
|
2000
|
+
requiredFields: string[] | undefined,
|
|
2001
|
+
): EnrichPersonBody {
|
|
2002
|
+
let firstName = readEntryField(entry, PEOPLE_FIRST_NAME_FIELDS);
|
|
2003
|
+
let lastName = readEntryField(entry, PEOPLE_LAST_NAME_FIELDS);
|
|
2004
|
+
if (!firstName || !lastName) {
|
|
2005
|
+
const fullName = readEntryField(entry, PEOPLE_FULL_NAME_FIELDS);
|
|
2006
|
+
if (fullName) {
|
|
2007
|
+
const split = splitFullName(fullName);
|
|
2008
|
+
firstName ??= split.firstName;
|
|
2009
|
+
lastName ??= split.lastName;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return {
|
|
2013
|
+
email: readEntryField(entry, PEOPLE_EMAIL_FIELDS),
|
|
2014
|
+
linkedinUrl: readEntryField(entry, PEOPLE_LINKEDIN_FIELDS),
|
|
2015
|
+
firstName,
|
|
2016
|
+
lastName,
|
|
2017
|
+
domain: readEntryField(entry, PEOPLE_DOMAIN_FIELDS),
|
|
2018
|
+
requiredFields,
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
export function buildCompanyBodyFromEntry(
|
|
2023
|
+
entry: CrmEntryForEnrichment,
|
|
2024
|
+
requiredFields: string[] | undefined,
|
|
2025
|
+
): EnrichCompanyBody {
|
|
2026
|
+
return {
|
|
2027
|
+
domain: readEntryField(entry, COMPANY_WEBSITE_FIELDS),
|
|
2028
|
+
companyName: readEntryField(entry, COMPANY_NAME_FIELDS),
|
|
2029
|
+
linkedinUrl: readEntryField(entry, COMPANY_LINKEDIN_FIELDS),
|
|
2030
|
+
requiredFields,
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
export function buildAviatoPersonBodyFromEntry(
|
|
2035
|
+
entry: CrmEntryForEnrichment,
|
|
2036
|
+
): AviatoEnrichPersonBody {
|
|
2037
|
+
return {
|
|
2038
|
+
linkedinUrl: readEntryField(entry, PEOPLE_LINKEDIN_FIELDS),
|
|
2039
|
+
email: readEntryField(entry, PEOPLE_EMAIL_FIELDS),
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
export function buildAviatoCompanyBodyFromEntry(
|
|
2044
|
+
entry: CrmEntryForEnrichment,
|
|
2045
|
+
): AviatoEnrichCompanyBody {
|
|
2046
|
+
return {
|
|
2047
|
+
website: readEntryField(entry, COMPANY_WEBSITE_FIELDS),
|
|
2048
|
+
linkedinUrl: readEntryField(entry, COMPANY_LINKEDIN_FIELDS),
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1772
2052
|
async function enrichOneEntry(
|
|
1773
2053
|
ctx: CrmCliContext,
|
|
1774
2054
|
args: Omit<CellEnrichmentArgs, "entryId"> & {
|
|
@@ -1776,6 +2056,7 @@ async function enrichOneEntry(
|
|
|
1776
2056
|
entry: CrmEntryForEnrichment;
|
|
1777
2057
|
},
|
|
1778
2058
|
): Promise<Record<string, unknown>> {
|
|
2059
|
+
let value: unknown;
|
|
1779
2060
|
try {
|
|
1780
2061
|
const payload = await callEnrichmentGatewayForEntry({
|
|
1781
2062
|
target: args.target,
|
|
@@ -1783,32 +2064,7 @@ async function enrichOneEntry(
|
|
|
1783
2064
|
provider: args.provider,
|
|
1784
2065
|
gateway: ctx.enrichmentGateway,
|
|
1785
2066
|
});
|
|
1786
|
-
|
|
1787
|
-
if (value === null) {
|
|
1788
|
-
return {
|
|
1789
|
-
entryId: args.entry.id,
|
|
1790
|
-
ok: false,
|
|
1791
|
-
reason: "value_not_found",
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
const writeResult = (await callMutation(
|
|
1795
|
-
ctx,
|
|
1796
|
-
api.enrich.applyCellEnrichmentResult,
|
|
1797
|
-
{
|
|
1798
|
-
objectName: args.objectName,
|
|
1799
|
-
entryId: args.entry.id,
|
|
1800
|
-
fieldName: args.fieldName,
|
|
1801
|
-
value,
|
|
1802
|
-
provider: args.provider ?? "gateway",
|
|
1803
|
-
overwrite: args.overwrite,
|
|
1804
|
-
},
|
|
1805
|
-
)) as Record<string, unknown>;
|
|
1806
|
-
return {
|
|
1807
|
-
entryId: args.entry.id,
|
|
1808
|
-
ok: writeResult.ok === true,
|
|
1809
|
-
value,
|
|
1810
|
-
...writeResult,
|
|
1811
|
-
};
|
|
2067
|
+
value = extractEnrichmentValue(payload, args.target.column);
|
|
1812
2068
|
} catch (error) {
|
|
1813
2069
|
if (
|
|
1814
2070
|
error instanceof EnrichmentGatewayError ||
|
|
@@ -1826,6 +2082,31 @@ async function enrichOneEntry(
|
|
|
1826
2082
|
}
|
|
1827
2083
|
throw error;
|
|
1828
2084
|
}
|
|
2085
|
+
if (value === null) {
|
|
2086
|
+
return {
|
|
2087
|
+
entryId: args.entry.id,
|
|
2088
|
+
ok: false,
|
|
2089
|
+
reason: "value_not_found",
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
const writeResult = (await callMutation(
|
|
2093
|
+
ctx,
|
|
2094
|
+
api.enrich.applyCellEnrichmentResult,
|
|
2095
|
+
{
|
|
2096
|
+
objectName: args.objectName,
|
|
2097
|
+
entryId: args.entry.id,
|
|
2098
|
+
fieldName: args.fieldName,
|
|
2099
|
+
value,
|
|
2100
|
+
provider: args.provider ?? "gateway",
|
|
2101
|
+
overwrite: args.overwrite,
|
|
2102
|
+
},
|
|
2103
|
+
)) as Record<string, unknown>;
|
|
2104
|
+
return {
|
|
2105
|
+
entryId: args.entry.id,
|
|
2106
|
+
ok: writeResult.ok === true,
|
|
2107
|
+
value,
|
|
2108
|
+
...writeResult,
|
|
2109
|
+
};
|
|
1829
2110
|
}
|
|
1830
2111
|
|
|
1831
2112
|
async function enrichCellThroughGateway(
|
|
@@ -2148,6 +2429,28 @@ Fields (columns):
|
|
|
2148
2429
|
dench crm fields delete <object> <field>
|
|
2149
2430
|
dench crm fields reorder <object> <field1> <field2> <field3> ...
|
|
2150
2431
|
|
|
2432
|
+
Enum option management (on existing enum fields):
|
|
2433
|
+
dench crm fields enum-reorder <object> <field> <value1> <value2> ...
|
|
2434
|
+
Canonical way to set the order of kanban columns / table picker options.
|
|
2435
|
+
The trailing positional values must be a permutation of the field's
|
|
2436
|
+
current enumValues; additions/removals are rejected (use enum-add /
|
|
2437
|
+
enum-remove first). Also keeps any matching crmStatuses rows in sync
|
|
2438
|
+
so the kanban (which prefers statuses over enumValues) reflects the
|
|
2439
|
+
new order.
|
|
2440
|
+
dench crm fields enum-add <object> <field> <value> [--color HEX] [--position N]
|
|
2441
|
+
Appends <value> to the field's enumValues. --color is a hex like
|
|
2442
|
+
"#22c55e"; --position is a 0-based insertion index (defaults to append).
|
|
2443
|
+
Idempotent: re-adding an existing value only updates its color when given.
|
|
2444
|
+
dench crm fields enum-remove <object> <field> <value>
|
|
2445
|
+
Removes the value from the option list. Historical entry rows that held
|
|
2446
|
+
<value> keep the literal string (the badge falls back to the default
|
|
2447
|
+
tint), matching Notion's "remove option, keep history" semantics.
|
|
2448
|
+
dench crm fields enum-rename <object> <field> <oldValue> <newValue>
|
|
2449
|
+
Renames the option AND rewrites every entry that referenced the old
|
|
2450
|
+
value (bounded to 1000 rows per call) AND renames matching crmStatuses.
|
|
2451
|
+
dench crm fields enum-set-color <object> <field> <value> <hex>
|
|
2452
|
+
Update the color (e.g. "#3b82f6") for an existing enum option.
|
|
2453
|
+
|
|
2151
2454
|
Entries (rows):
|
|
2152
2455
|
dench crm entries list <object> [--limit N] [--with-meta] [--json]
|
|
2153
2456
|
dench crm entries get <object> <entryId>
|
package/dench.ts
CHANGED
|
@@ -376,6 +376,30 @@ function parseCommaOption(name: string) {
|
|
|
376
376
|
);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function parseMemoryVisibilityOption() {
|
|
380
|
+
const scope = option("--scope");
|
|
381
|
+
const visibility = option("--visibility");
|
|
382
|
+
if (scope && visibility && scope !== visibility) {
|
|
383
|
+
throw new CliError(
|
|
384
|
+
"--scope and --visibility must match when both are set",
|
|
385
|
+
{
|
|
386
|
+
code: "memory_scope_conflict",
|
|
387
|
+
nextActions: ["Use only --scope user or --scope org."],
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const value = scope ?? visibility ?? "user";
|
|
392
|
+
if (value !== "user" && value !== "org") {
|
|
393
|
+
throw new CliError("--scope must be user or org", {
|
|
394
|
+
code: "invalid_memory_scope",
|
|
395
|
+
nextActions: [
|
|
396
|
+
"Use --scope user for private user memory or --scope org for shared organization memory.",
|
|
397
|
+
],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
|
|
379
403
|
function logHuman(message: string) {
|
|
380
404
|
if (!json) {
|
|
381
405
|
console.error(message);
|
|
@@ -398,7 +422,7 @@ Usage:
|
|
|
398
422
|
dench context [--json]
|
|
399
423
|
dench status [--mine|--self] [--json]
|
|
400
424
|
dench memory search "query" [--limit 8] [--json]
|
|
401
|
-
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--json]
|
|
425
|
+
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--scope user|org] [--json]
|
|
402
426
|
dench artifacts [--limit 25] [--json]
|
|
403
427
|
dench suggested-work [--json]
|
|
404
428
|
dench approval request "message" [--json]
|
|
@@ -610,10 +634,11 @@ function memoryHelp() {
|
|
|
610
634
|
|
|
611
635
|
Usage:
|
|
612
636
|
dench memory search "query" [--limit 8] [--json]
|
|
613
|
-
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--json]
|
|
637
|
+
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--scope user|org] [--json]
|
|
614
638
|
|
|
615
639
|
Use memory only for stable facts, decisions, preferences, recurring goals, and
|
|
616
|
-
tool notes. Do not store secrets or scratch notes.
|
|
640
|
+
tool notes. Do not store secrets or scratch notes. Saved memories are scoped to
|
|
641
|
+
the signed-in user by default; pass --scope org to share with the organization.
|
|
617
642
|
`);
|
|
618
643
|
}
|
|
619
644
|
|
|
@@ -2419,6 +2444,7 @@ function requireAuthenticatedRuntime(
|
|
|
2419
2444
|
type GatewayCommandAuth = {
|
|
2420
2445
|
bearerToken: string;
|
|
2421
2446
|
gatewayBaseUrl?: string;
|
|
2447
|
+
gatewayHeaders?: Record<string, string>;
|
|
2422
2448
|
};
|
|
2423
2449
|
|
|
2424
2450
|
function gatewayBaseUrlForCli(value: unknown): string | undefined {
|
|
@@ -2515,6 +2541,12 @@ async function resolveGatewayCommandAuth(
|
|
|
2515
2541
|
return {
|
|
2516
2542
|
bearerToken: gatewayKey,
|
|
2517
2543
|
gatewayBaseUrl: gatewayBaseUrlForCli(body.gatewayBaseUrl),
|
|
2544
|
+
gatewayHeaders:
|
|
2545
|
+
body.gatewayHeaders &&
|
|
2546
|
+
typeof body.gatewayHeaders === "object" &&
|
|
2547
|
+
!Array.isArray(body.gatewayHeaders)
|
|
2548
|
+
? (body.gatewayHeaders as Record<string, string>)
|
|
2549
|
+
: undefined,
|
|
2518
2550
|
};
|
|
2519
2551
|
}
|
|
2520
2552
|
|
|
@@ -2622,6 +2654,7 @@ async function connectedAppsContext(runtime: Runtime) {
|
|
|
2622
2654
|
return fetchConnectedAppsSummary({
|
|
2623
2655
|
bearerToken: gatewayAuth.bearerToken,
|
|
2624
2656
|
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
2657
|
+
gatewayHeaders: gatewayAuth.gatewayHeaders,
|
|
2625
2658
|
});
|
|
2626
2659
|
} catch (error) {
|
|
2627
2660
|
return {
|
|
@@ -3822,6 +3855,7 @@ async function main() {
|
|
|
3822
3855
|
jsonOutput: json,
|
|
3823
3856
|
bearerToken: gatewayAuth.bearerToken,
|
|
3824
3857
|
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3858
|
+
gatewayHeaders: gatewayAuth.gatewayHeaders,
|
|
3825
3859
|
});
|
|
3826
3860
|
return;
|
|
3827
3861
|
}
|
|
@@ -3841,6 +3875,7 @@ async function main() {
|
|
|
3841
3875
|
jsonOutput: json,
|
|
3842
3876
|
bearerToken: gatewayAuth.bearerToken,
|
|
3843
3877
|
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3878
|
+
gatewayHeaders: gatewayAuth.gatewayHeaders,
|
|
3844
3879
|
});
|
|
3845
3880
|
return;
|
|
3846
3881
|
}
|
|
@@ -4135,6 +4170,7 @@ async function main() {
|
|
|
4135
4170
|
text,
|
|
4136
4171
|
tags: parseCommaOption("--tags"),
|
|
4137
4172
|
sensitivity: (option("--sensitivity") ?? "normal") as never,
|
|
4173
|
+
visibility: parseMemoryVisibilityOption() as never,
|
|
4138
4174
|
},
|
|
4139
4175
|
),
|
|
4140
4176
|
);
|
|
@@ -1280,6 +1280,28 @@ async function buildGatewayError(
|
|
|
1280
1280
|
code: code ?? "not_found",
|
|
1281
1281
|
});
|
|
1282
1282
|
}
|
|
1283
|
+
if (
|
|
1284
|
+
response.status === 401 ||
|
|
1285
|
+
code === "invalid_api_key" ||
|
|
1286
|
+
code === "unauthorized"
|
|
1287
|
+
) {
|
|
1288
|
+
return new EnrichmentGatewayError(
|
|
1289
|
+
upstreamMessage ?? "Unauthorized gateway request",
|
|
1290
|
+
{ status: response.status, code: "unauthorized" },
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
if (response.status === 403 || code === "forbidden") {
|
|
1294
|
+
return new EnrichmentGatewayError(
|
|
1295
|
+
upstreamMessage ?? "Forbidden gateway request",
|
|
1296
|
+
{ status: response.status, code: "forbidden" },
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
if (code === "aviato_unauthorized") {
|
|
1300
|
+
return new EnrichmentGatewayError(
|
|
1301
|
+
upstreamMessage ?? "Aviato provider unauthorized",
|
|
1302
|
+
{ status: response.status || 401, code: "aviato_unauthorized" },
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1283
1305
|
if (response.status === 503 || code === "provider_unavailable") {
|
|
1284
1306
|
return new EnrichmentGatewayError("Gateway providers unavailable", {
|
|
1285
1307
|
status: response.status,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/tools.ts
CHANGED
|
@@ -50,11 +50,12 @@ export type ToolCliContext = {
|
|
|
50
50
|
jsonOutput: boolean;
|
|
51
51
|
bearerToken?: string;
|
|
52
52
|
gatewayBaseUrl?: string;
|
|
53
|
+
gatewayHeaders?: Record<string, string>;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
type GatewayAuthContext = Pick<
|
|
56
57
|
ToolCliContext,
|
|
57
|
-
"bearerToken" | "gatewayBaseUrl"
|
|
58
|
+
"bearerToken" | "gatewayBaseUrl" | "gatewayHeaders"
|
|
58
59
|
>;
|
|
59
60
|
|
|
60
61
|
export async function runToolCommand(ctx: ToolCliContext): Promise<void> {
|
|
@@ -453,6 +454,7 @@ async function callGateway(
|
|
|
453
454
|
headers: {
|
|
454
455
|
"content-type": "application/json",
|
|
455
456
|
authorization: `Bearer ${bearerToken}`,
|
|
457
|
+
...(ctx.gatewayHeaders ?? {}),
|
|
456
458
|
},
|
|
457
459
|
};
|
|
458
460
|
if (body !== undefined) init.body = JSON.stringify(body);
|