@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.
Files changed (4) hide show
  1. package/crm.ts +289 -31
  2. package/dench.ts +39 -3
  3. package/package.json +1 -1
  4. 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
- enrichPersonByIdentifier,
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 input = args.entry.fields[args.target.inputField.name];
1715
- if (!hasCellValue(input)) {
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 ${args.entry.id} has no value for input field "${args.target.inputField.name}"`,
1853
+ `Entry ${entry.id} has no value for input field "${target.inputField.name}"`,
1718
1854
  );
1719
1855
  }
1720
- const inputValue = String(input);
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
- // Guard: Aviato does not return phone or email.
1727
- const contactFields = args.target.requiredFields.filter(isContactField);
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
- isLinkedin ? { linkedinUrl: inputValue } : { website: inputValue },
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
- // fullenrich path (default for people, explicit override for companies)
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 enrichPersonByIdentifier(
1766
- inputValue,
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.4",
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);