@dench.com/cli 2.0.4 → 2.1.0
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 +289 -31
- package/dench.ts +39 -3
- 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.
|
|
@@ -383,6 +387,21 @@ const api = {
|
|
|
383
387
|
update: makeFunctionReference<"mutation">("functions/crm/fields:update"),
|
|
384
388
|
remove: makeFunctionReference<"mutation">("functions/crm/fields:remove"),
|
|
385
389
|
reorder: makeFunctionReference<"mutation">("functions/crm/fields:reorder"),
|
|
390
|
+
reorderEnumValues: makeFunctionReference<"mutation">(
|
|
391
|
+
"functions/crm/fields:reorderEnumValues",
|
|
392
|
+
),
|
|
393
|
+
addEnumValue: makeFunctionReference<"mutation">(
|
|
394
|
+
"functions/crm/fields:addEnumValue",
|
|
395
|
+
),
|
|
396
|
+
removeEnumValue: makeFunctionReference<"mutation">(
|
|
397
|
+
"functions/crm/fields:removeEnumValue",
|
|
398
|
+
),
|
|
399
|
+
setEnumColor: makeFunctionReference<"mutation">(
|
|
400
|
+
"functions/crm/fields:setEnumColor",
|
|
401
|
+
),
|
|
402
|
+
renameEnumValue: makeFunctionReference<"mutation">(
|
|
403
|
+
"functions/crm/fields:renameEnumValue",
|
|
404
|
+
),
|
|
386
405
|
},
|
|
387
406
|
entries: {
|
|
388
407
|
list: makeFunctionReference<"query">("functions/crm/entries:list"),
|
|
@@ -693,6 +712,105 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
693
712
|
);
|
|
694
713
|
return;
|
|
695
714
|
}
|
|
715
|
+
case "enum-reorder": {
|
|
716
|
+
const objectName = shift(ctx.args, "object name");
|
|
717
|
+
const fieldName = shift(ctx.args, "field name");
|
|
718
|
+
// Same pattern as `reorder`: peel positional args after asserting
|
|
719
|
+
// there are no stray flags so a typo'd `--limit` fails loud
|
|
720
|
+
// instead of being silently consumed as an enum value.
|
|
721
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-reorder");
|
|
722
|
+
const orderedValues = [...ctx.args];
|
|
723
|
+
if (orderedValues.length === 0) {
|
|
724
|
+
throw new CrmCliError(
|
|
725
|
+
"dench crm fields enum-reorder requires at least one value. " +
|
|
726
|
+
'Pass each enum value as a positional arg: `enum-reorder task Status "Done" "In Progress" "To Do"`',
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
out(
|
|
730
|
+
ctx,
|
|
731
|
+
await callMutation(ctx, api.fields.reorderEnumValues, {
|
|
732
|
+
objectName,
|
|
733
|
+
fieldName,
|
|
734
|
+
orderedValues,
|
|
735
|
+
}),
|
|
736
|
+
);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
case "enum-add": {
|
|
740
|
+
const objectName = shift(ctx.args, "object name");
|
|
741
|
+
const fieldName = shift(ctx.args, "field name");
|
|
742
|
+
const value = shift(ctx.args, "enum value");
|
|
743
|
+
const color = getFlag(ctx.args, "--color");
|
|
744
|
+
const positionStr = getFlag(ctx.args, "--position");
|
|
745
|
+
const position =
|
|
746
|
+
positionStr !== undefined ? Number.parseInt(positionStr, 10) : undefined;
|
|
747
|
+
if (position !== undefined && !Number.isFinite(position)) {
|
|
748
|
+
throw new CrmCliError(
|
|
749
|
+
`--position must be an integer, got "${positionStr}"`,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-add");
|
|
753
|
+
out(
|
|
754
|
+
ctx,
|
|
755
|
+
await callMutation(ctx, api.fields.addEnumValue, {
|
|
756
|
+
objectName,
|
|
757
|
+
fieldName,
|
|
758
|
+
value,
|
|
759
|
+
color,
|
|
760
|
+
position,
|
|
761
|
+
}),
|
|
762
|
+
);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
case "enum-remove": {
|
|
766
|
+
const objectName = shift(ctx.args, "object name");
|
|
767
|
+
const fieldName = shift(ctx.args, "field name");
|
|
768
|
+
const value = shift(ctx.args, "enum value");
|
|
769
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-remove");
|
|
770
|
+
out(
|
|
771
|
+
ctx,
|
|
772
|
+
await callMutation(ctx, api.fields.removeEnumValue, {
|
|
773
|
+
objectName,
|
|
774
|
+
fieldName,
|
|
775
|
+
value,
|
|
776
|
+
}),
|
|
777
|
+
);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
case "enum-rename": {
|
|
781
|
+
const objectName = shift(ctx.args, "object name");
|
|
782
|
+
const fieldName = shift(ctx.args, "field name");
|
|
783
|
+
const oldValue = shift(ctx.args, "old enum value");
|
|
784
|
+
const newValue = shift(ctx.args, "new enum value");
|
|
785
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-rename");
|
|
786
|
+
out(
|
|
787
|
+
ctx,
|
|
788
|
+
await callMutation(ctx, api.fields.renameEnumValue, {
|
|
789
|
+
objectName,
|
|
790
|
+
fieldName,
|
|
791
|
+
oldValue,
|
|
792
|
+
newValue,
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
case "enum-set-color": {
|
|
798
|
+
const objectName = shift(ctx.args, "object name");
|
|
799
|
+
const fieldName = shift(ctx.args, "field name");
|
|
800
|
+
const value = shift(ctx.args, "enum value");
|
|
801
|
+
const color = shift(ctx.args, "hex color");
|
|
802
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-set-color");
|
|
803
|
+
out(
|
|
804
|
+
ctx,
|
|
805
|
+
await callMutation(ctx, api.fields.setEnumColor, {
|
|
806
|
+
objectName,
|
|
807
|
+
fieldName,
|
|
808
|
+
value,
|
|
809
|
+
color,
|
|
810
|
+
}),
|
|
811
|
+
);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
696
814
|
default:
|
|
697
815
|
throw new CrmCliError(`Unknown crm fields verb: ${verb ?? "<none>"}`);
|
|
698
816
|
}
|
|
@@ -1704,6 +1822,21 @@ async function resolveEnrichmentTarget(
|
|
|
1704
1822
|
};
|
|
1705
1823
|
}
|
|
1706
1824
|
|
|
1825
|
+
/**
|
|
1826
|
+
* Build the gateway request body from a CRM entry the same way the
|
|
1827
|
+
* chat-turn `enrich_person` / `enrich_company` / `enrich_person_basic` /
|
|
1828
|
+
* `enrich_company_basic` tools do — pass every structured identifier the
|
|
1829
|
+
* row has (email, firstName, lastName, linkedinUrl, domain, …) and let the
|
|
1830
|
+
* shared gateway client (`enrichPerson`, `enrichCompany`,
|
|
1831
|
+
* `aviatoEnrichPerson`, `aviatoEnrichCompany`) validate + dispatch.
|
|
1832
|
+
*
|
|
1833
|
+
* The previous implementation collapsed the entry to a single
|
|
1834
|
+
* `--input-field` value and called `enrichPersonByIdentifier`, which
|
|
1835
|
+
* threw away the rest of the row and made FullEnrich fail with
|
|
1836
|
+
* `missing_people_identifier` whenever the input field was just an
|
|
1837
|
+
* Email (no first/last name reached the gateway, so the validator
|
|
1838
|
+
* rejected the request before any provider was called).
|
|
1839
|
+
*/
|
|
1707
1840
|
async function callEnrichmentGatewayForEntry(args: {
|
|
1708
1841
|
target: EnrichmentTarget;
|
|
1709
1842
|
entry: CrmEntryForEnrichment;
|
|
@@ -1711,64 +1844,167 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1711
1844
|
provider?: string;
|
|
1712
1845
|
gateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
1713
1846
|
}): Promise<Record<string, unknown>> {
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1847
|
+
const { entry, target } = args;
|
|
1848
|
+
// Sanity gate: the column the user picked as `--input-field` must be
|
|
1849
|
+
// populated. The body builder will pass every other available identifier
|
|
1850
|
+
// too, so the gateway has the best possible chance of resolving the row.
|
|
1851
|
+
if (!hasCellValue(entry.fields[target.inputField.name])) {
|
|
1716
1852
|
throw new CrmCliError(
|
|
1717
|
-
`Entry ${
|
|
1853
|
+
`Entry ${entry.id} has no value for input field "${target.inputField.name}"`,
|
|
1718
1854
|
);
|
|
1719
1855
|
}
|
|
1720
|
-
const
|
|
1721
|
-
const isCompany = args.target.category === "company";
|
|
1856
|
+
const isCompany = target.category === "company";
|
|
1722
1857
|
const resolvedProvider =
|
|
1723
1858
|
args.provider ?? (isCompany ? "aviato" : "fullenrich");
|
|
1724
1859
|
|
|
1725
1860
|
if (resolvedProvider === "aviato") {
|
|
1726
|
-
//
|
|
1727
|
-
|
|
1861
|
+
// Aviato does not return phone or email — surface a clear error
|
|
1862
|
+
// before we waste a gateway round-trip.
|
|
1863
|
+
const contactFields = target.requiredFields.filter(isContactField);
|
|
1728
1864
|
if (contactFields.length > 0) {
|
|
1729
1865
|
throw new CrmCliError(
|
|
1730
1866
|
`Aviato does not return phone or email; use --provider fullenrich for contact fields (${contactFields.join(", ")}).`,
|
|
1731
1867
|
);
|
|
1732
1868
|
}
|
|
1733
|
-
|
|
1734
1869
|
if (isCompany) {
|
|
1735
|
-
// inputValue is typically a domain; try to detect linkedin URLs
|
|
1736
|
-
const isLinkedin = /linkedin\.com/i.test(inputValue);
|
|
1737
1870
|
return aviatoEnrichCompany(
|
|
1738
|
-
|
|
1871
|
+
buildAviatoCompanyBodyFromEntry(entry),
|
|
1739
1872
|
args.gateway,
|
|
1740
1873
|
);
|
|
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
1874
|
}
|
|
1875
|
+
return aviatoEnrichPerson(
|
|
1876
|
+
buildAviatoPersonBodyFromEntry(entry),
|
|
1877
|
+
args.gateway,
|
|
1878
|
+
);
|
|
1753
1879
|
}
|
|
1754
1880
|
|
|
1755
|
-
//
|
|
1881
|
+
// FullEnrich path. Mirrors `enrichPersonStep` / `enrichCompanyStep` in
|
|
1882
|
+
// src/workflows/enrichment-steps.ts: build a structured body, hand it
|
|
1883
|
+
// to the same shared gateway client.
|
|
1756
1884
|
if (isCompany) {
|
|
1757
1885
|
return enrichCompany(
|
|
1758
|
-
|
|
1759
|
-
domain: inputValue,
|
|
1760
|
-
requiredFields: args.target.requiredFields,
|
|
1761
|
-
},
|
|
1886
|
+
buildCompanyBodyFromEntry(entry, target.requiredFields),
|
|
1762
1887
|
args.gateway,
|
|
1763
1888
|
);
|
|
1764
1889
|
}
|
|
1765
|
-
return
|
|
1766
|
-
|
|
1767
|
-
args.target.requiredFields,
|
|
1890
|
+
return enrichPerson(
|
|
1891
|
+
buildPersonBodyFromEntry(entry, target.requiredFields),
|
|
1768
1892
|
args.gateway,
|
|
1769
1893
|
);
|
|
1770
1894
|
}
|
|
1771
1895
|
|
|
1896
|
+
// ── Body builders ────────────────────────────────────────────────────────
|
|
1897
|
+
//
|
|
1898
|
+
// Pick whichever canonical column name the entry happens to use. Field
|
|
1899
|
+
// naming on CRM objects has historically drifted (`Name` vs `Full Name`,
|
|
1900
|
+
// `Email` vs `Email Address`, `Website` vs `Domain`), so each builder
|
|
1901
|
+
// accepts a small list of synonyms before giving up.
|
|
1902
|
+
|
|
1903
|
+
const PEOPLE_FULL_NAME_FIELDS = ["Name", "Full Name", "Person Name"] as const;
|
|
1904
|
+
const PEOPLE_FIRST_NAME_FIELDS = ["First Name", "Given Name"] as const;
|
|
1905
|
+
const PEOPLE_LAST_NAME_FIELDS = ["Last Name", "Family Name", "Surname"] as const;
|
|
1906
|
+
const PEOPLE_EMAIL_FIELDS = [
|
|
1907
|
+
"Email",
|
|
1908
|
+
"Email Address",
|
|
1909
|
+
"Work Email",
|
|
1910
|
+
"Personal Email",
|
|
1911
|
+
] as const;
|
|
1912
|
+
const PEOPLE_LINKEDIN_FIELDS = ["LinkedIn URL", "LinkedIn", "Linkedin URL"] as const;
|
|
1913
|
+
const PEOPLE_DOMAIN_FIELDS = ["Domain", "Company Domain"] as const;
|
|
1914
|
+
|
|
1915
|
+
const COMPANY_WEBSITE_FIELDS = ["Website", "Domain", "URL", "Site"] as const;
|
|
1916
|
+
const COMPANY_LINKEDIN_FIELDS = [
|
|
1917
|
+
"LinkedIn URL",
|
|
1918
|
+
"LinkedIn",
|
|
1919
|
+
"Linkedin URL",
|
|
1920
|
+
] as const;
|
|
1921
|
+
const COMPANY_NAME_FIELDS = [
|
|
1922
|
+
"Company Name",
|
|
1923
|
+
"Name",
|
|
1924
|
+
"Organization",
|
|
1925
|
+
"Organization Name",
|
|
1926
|
+
] as const;
|
|
1927
|
+
|
|
1928
|
+
function readEntryField(
|
|
1929
|
+
entry: CrmEntryForEnrichment,
|
|
1930
|
+
candidates: readonly string[],
|
|
1931
|
+
): string | undefined {
|
|
1932
|
+
for (const name of candidates) {
|
|
1933
|
+
const value = entry.fields[name];
|
|
1934
|
+
if (typeof value === "string") {
|
|
1935
|
+
const trimmed = value.trim();
|
|
1936
|
+
if (trimmed.length > 0) return trimmed;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return undefined;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/** Split "Mendy Edri" → { firstName: "Mendy", lastName: "Edri" }. */
|
|
1943
|
+
export function splitFullName(fullName: string): {
|
|
1944
|
+
firstName?: string;
|
|
1945
|
+
lastName?: string;
|
|
1946
|
+
} {
|
|
1947
|
+
const trimmed = fullName.trim();
|
|
1948
|
+
if (!trimmed) return {};
|
|
1949
|
+
const parts = trimmed.split(/\s+/);
|
|
1950
|
+
if (parts.length === 1) return { firstName: parts[0] };
|
|
1951
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
export function buildPersonBodyFromEntry(
|
|
1955
|
+
entry: CrmEntryForEnrichment,
|
|
1956
|
+
requiredFields: string[] | undefined,
|
|
1957
|
+
): EnrichPersonBody {
|
|
1958
|
+
let firstName = readEntryField(entry, PEOPLE_FIRST_NAME_FIELDS);
|
|
1959
|
+
let lastName = readEntryField(entry, PEOPLE_LAST_NAME_FIELDS);
|
|
1960
|
+
if (!firstName || !lastName) {
|
|
1961
|
+
const fullName = readEntryField(entry, PEOPLE_FULL_NAME_FIELDS);
|
|
1962
|
+
if (fullName) {
|
|
1963
|
+
const split = splitFullName(fullName);
|
|
1964
|
+
firstName ??= split.firstName;
|
|
1965
|
+
lastName ??= split.lastName;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
email: readEntryField(entry, PEOPLE_EMAIL_FIELDS),
|
|
1970
|
+
linkedinUrl: readEntryField(entry, PEOPLE_LINKEDIN_FIELDS),
|
|
1971
|
+
firstName,
|
|
1972
|
+
lastName,
|
|
1973
|
+
domain: readEntryField(entry, PEOPLE_DOMAIN_FIELDS),
|
|
1974
|
+
requiredFields,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
export function buildCompanyBodyFromEntry(
|
|
1979
|
+
entry: CrmEntryForEnrichment,
|
|
1980
|
+
requiredFields: string[] | undefined,
|
|
1981
|
+
): EnrichCompanyBody {
|
|
1982
|
+
return {
|
|
1983
|
+
domain: readEntryField(entry, COMPANY_WEBSITE_FIELDS),
|
|
1984
|
+
companyName: readEntryField(entry, COMPANY_NAME_FIELDS),
|
|
1985
|
+
linkedinUrl: readEntryField(entry, COMPANY_LINKEDIN_FIELDS),
|
|
1986
|
+
requiredFields,
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
export function buildAviatoPersonBodyFromEntry(
|
|
1991
|
+
entry: CrmEntryForEnrichment,
|
|
1992
|
+
): AviatoEnrichPersonBody {
|
|
1993
|
+
return {
|
|
1994
|
+
linkedinUrl: readEntryField(entry, PEOPLE_LINKEDIN_FIELDS),
|
|
1995
|
+
email: readEntryField(entry, PEOPLE_EMAIL_FIELDS),
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
export function buildAviatoCompanyBodyFromEntry(
|
|
2000
|
+
entry: CrmEntryForEnrichment,
|
|
2001
|
+
): AviatoEnrichCompanyBody {
|
|
2002
|
+
return {
|
|
2003
|
+
website: readEntryField(entry, COMPANY_WEBSITE_FIELDS),
|
|
2004
|
+
linkedinUrl: readEntryField(entry, COMPANY_LINKEDIN_FIELDS),
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1772
2008
|
async function enrichOneEntry(
|
|
1773
2009
|
ctx: CrmCliContext,
|
|
1774
2010
|
args: Omit<CellEnrichmentArgs, "entryId"> & {
|
|
@@ -2148,6 +2384,28 @@ Fields (columns):
|
|
|
2148
2384
|
dench crm fields delete <object> <field>
|
|
2149
2385
|
dench crm fields reorder <object> <field1> <field2> <field3> ...
|
|
2150
2386
|
|
|
2387
|
+
Enum option management (on existing enum fields):
|
|
2388
|
+
dench crm fields enum-reorder <object> <field> <value1> <value2> ...
|
|
2389
|
+
Canonical way to set the order of kanban columns / table picker options.
|
|
2390
|
+
The trailing positional values must be a permutation of the field's
|
|
2391
|
+
current enumValues; additions/removals are rejected (use enum-add /
|
|
2392
|
+
enum-remove first). Also keeps any matching crmStatuses rows in sync
|
|
2393
|
+
so the kanban (which prefers statuses over enumValues) reflects the
|
|
2394
|
+
new order.
|
|
2395
|
+
dench crm fields enum-add <object> <field> <value> [--color HEX] [--position N]
|
|
2396
|
+
Appends <value> to the field's enumValues. --color is a hex like
|
|
2397
|
+
"#22c55e"; --position is a 0-based insertion index (defaults to append).
|
|
2398
|
+
Idempotent: re-adding an existing value only updates its color when given.
|
|
2399
|
+
dench crm fields enum-remove <object> <field> <value>
|
|
2400
|
+
Removes the value from the option list. Historical entry rows that held
|
|
2401
|
+
<value> keep the literal string (the badge falls back to the default
|
|
2402
|
+
tint), matching Notion's "remove option, keep history" semantics.
|
|
2403
|
+
dench crm fields enum-rename <object> <field> <oldValue> <newValue>
|
|
2404
|
+
Renames the option AND rewrites every entry that referenced the old
|
|
2405
|
+
value (bounded to 1000 rows per call) AND renames matching crmStatuses.
|
|
2406
|
+
dench crm fields enum-set-color <object> <field> <value> <hex>
|
|
2407
|
+
Update the color (e.g. "#3b82f6") for an existing enum option.
|
|
2408
|
+
|
|
2151
2409
|
Entries (rows):
|
|
2152
2410
|
dench crm entries list <object> [--limit N] [--with-meta] [--json]
|
|
2153
2411
|
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
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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);
|