@dench.com/cli 2.0.3 → 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/agent-config.ts CHANGED
@@ -34,11 +34,51 @@ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
34
34
  import { tmpdir } from "node:os";
35
35
  import { join } from "node:path";
36
36
  import type { ConvexHttpClient } from "convex/browser";
37
- import { api } from "@/../convex/_generated/api";
37
+ import { makeFunctionReference } from "convex/server";
38
38
  import { CliArgError, getFlag, hasFlag } from "./lib/cli-args";
39
39
 
40
40
  class AgentConfigCliError extends Error {}
41
41
 
42
+ const api = {
43
+ functions: {
44
+ agentConfig: {
45
+ getAgentConfig: makeFunctionReference<"query">(
46
+ "functions/agentConfig:getAgentConfig",
47
+ ),
48
+ updateIdentity: makeFunctionReference<"mutation">(
49
+ "functions/agentConfig:updateIdentity",
50
+ ),
51
+ updateOrganisation: makeFunctionReference<"mutation">(
52
+ "functions/agentConfig:updateOrganisation",
53
+ ),
54
+ updateMemory: makeFunctionReference<"mutation">(
55
+ "functions/agentConfig:updateMemory",
56
+ ),
57
+ updateToolsNotes: makeFunctionReference<"mutation">(
58
+ "functions/agentConfig:updateToolsNotes",
59
+ ),
60
+ updateUserProfile: makeFunctionReference<"mutation">(
61
+ "functions/agentConfig:updateUserProfile",
62
+ ),
63
+ updateHeartbeat: makeFunctionReference<"mutation">(
64
+ "functions/agentConfig:updateHeartbeat",
65
+ ),
66
+ updateBootstrapTemplate: makeFunctionReference<"mutation">(
67
+ "functions/agentConfig:updateBootstrapTemplate",
68
+ ),
69
+ completeBootstrap: makeFunctionReference<"mutation">(
70
+ "functions/agentConfig:completeBootstrap",
71
+ ),
72
+ setDefaultChatModelV2: makeFunctionReference<"mutation">(
73
+ "functions/agentConfig:setDefaultChatModelV2",
74
+ ),
75
+ setThreadModelOverride: makeFunctionReference<"mutation">(
76
+ "functions/agentConfig:setThreadModelOverride",
77
+ ),
78
+ },
79
+ },
80
+ };
81
+
42
82
  type CliCtx = {
43
83
  convex: ConvexHttpClient;
44
84
  args: string[];
@@ -86,7 +126,9 @@ async function readContentFromStdin(): Promise<string> {
86
126
  return new Promise((resolve, reject) => {
87
127
  const chunks: Buffer[] = [];
88
128
  process.stdin.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
89
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
129
+ process.stdin.on("end", () =>
130
+ resolve(Buffer.concat(chunks).toString("utf8")),
131
+ );
90
132
  process.stdin.on("error", reject);
91
133
  });
92
134
  }
@@ -150,7 +192,10 @@ async function loadAgentConfig(ctx: CliCtx): Promise<{
150
192
  userProfile: { content: string; updatedAt: number } | null;
151
193
  organizationId: string;
152
194
  }> {
153
- const result = (await runQuery(ctx, api.functions.agentConfig.getAgentConfig)) as {
195
+ const result = (await runQuery(
196
+ ctx,
197
+ api.functions.agentConfig.getAgentConfig,
198
+ )) as {
154
199
  agentConfig: {
155
200
  identity: { content: string; updatedAt: number } | null;
156
201
  organisation: { content: string; updatedAt: number } | null;
@@ -212,14 +257,14 @@ async function handleSimpleMagicFile(
212
257
  } else {
213
258
  const config = await loadAgentConfig(ctx);
214
259
  const initial = opts.perMember
215
- ? config.userProfile?.content ?? ""
260
+ ? (config.userProfile?.content ?? "")
216
261
  : opts.field === "memoryAggregate"
217
- ? config.agentConfig.memoryAggregate?.content ?? ""
262
+ ? (config.agentConfig.memoryAggregate?.content ?? "")
218
263
  : opts.field === "toolsNotes"
219
- ? config.agentConfig.toolsNotes?.content ?? ""
264
+ ? (config.agentConfig.toolsNotes?.content ?? "")
220
265
  : opts.field === "identity"
221
- ? config.agentConfig.identity?.content ?? ""
222
- : config.agentConfig.organisation?.content ?? "";
266
+ ? (config.agentConfig.identity?.content ?? "")
267
+ : (config.agentConfig.organisation?.content ?? "");
223
268
  content = openEditor(initial, opts.label);
224
269
  }
225
270
  if (!content.trim().length) {
@@ -519,8 +564,7 @@ export async function runMemoryAggregateCommand(ctx: CliCtx): Promise<void> {
519
564
  }
520
565
  if (sub === "append") {
521
566
  const text = ctx.args.slice(1).join(" ").trim();
522
- if (!text)
523
- throw new AgentConfigCliError("Usage: dench mem append <text>");
567
+ if (!text) throw new AgentConfigCliError("Usage: dench mem append <text>");
524
568
  await runMutation(ctx, api.functions.agentConfig.updateMemory, {
525
569
  content: text,
526
570
  append: true,
@@ -593,9 +637,7 @@ export async function runDailyCommand(ctx: CliCtx): Promise<void> {
593
637
  if (sub === "show") {
594
638
  const date = ctx.args[1];
595
639
  if (!date) {
596
- throw new AgentConfigCliError(
597
- "Usage: dench daily show <YYYY-MM-DD>",
598
- );
640
+ throw new AgentConfigCliError("Usage: dench daily show <YYYY-MM-DD>");
599
641
  }
600
642
  out(
601
643
  ctx,
package/chat-spawn.ts CHANGED
@@ -16,8 +16,6 @@
16
16
  * (`/api/chat/[runId]/stream`) and pretty-prints text deltas + tool
17
17
  * calls until the workflow finishes.
18
18
  */
19
-
20
- import { getCurrentRuntimeTimeZone } from "@/lib/timezone";
21
19
  import { hasFlag } from "./lib/cli-args";
22
20
 
23
21
  class ChatSpawnError extends Error {}
@@ -33,6 +31,18 @@ type ChatSpawnContext = {
33
31
  jsonOutput: boolean;
34
32
  };
35
33
 
34
+ function getCurrentRuntimeTimeZone(): string | null {
35
+ try {
36
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
37
+ if (!timeZone || timeZone.length > 100) return null;
38
+ // Validate that Intl accepts the resolved value before forwarding it.
39
+ Intl.DateTimeFormat("en-US", { timeZone }).format(new Date());
40
+ return timeZone;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
36
46
  function consumeFlagValue(args: string[], name: string): string | undefined {
37
47
  const idx = args.indexOf(name);
38
48
  if (idx === -1) return undefined;
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]
@@ -440,13 +464,13 @@ Workspace members (for CRM \`user\`-type fields):
440
464
  cells assign to real members instead of being stored as unlinked
441
465
  display-name strings. See \`dench members help\`.
442
466
 
443
- Live web search (Exa via the Dench Cloud Gateway, auths with the active \`dench signin\` agent session or DENCH_API_KEY):
467
+ Live web search (Exa via the Dench Gateway, auths with the active \`dench signin\` agent session or DENCH_API_KEY):
444
468
  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]
445
469
  dench search contents <url> [<url>...] [--max-chars 4000] [--summary "..."] [--json]
446
470
  dench search answer "<query>" [--json]
447
471
  Help: dench search help
448
472
 
449
- Image generation / editing (gpt-image-2 via the Dench Cloud Gateway):
473
+ Image generation / editing (via the Dench Gateway):
450
474
  dench image generate "<prompt>" [--size 1024x1024] [--quality auto|low|medium|high] [--format png|jpeg|webp] [--save-path /workspace/path/file.png] [--output <local file>] [--no-write] [--json]
451
475
  dench image edit "<prompt>" --input <path> [--input <path> ...] [--mask <path>] [--size ...] [--quality ...] [--format ...] [--save-path ...] [--output ...] [--json]
452
476
  Help: dench image help
@@ -472,7 +496,7 @@ Spawn a new chat-turn workflow (same as the web UI):
472
496
 
473
497
  Help: dench chat help
474
498
 
475
- File sync (Daytona-native rewrite):
499
+ File sync:
476
500
  dench fs status [--json] [--workspace /workspace] [--no-hash] [--drift-limit N]
477
501
  dench fs sync --initial --org-id <orgId> [--workspace /workspace]
478
502
  dench fs stream-open <path>
@@ -488,7 +512,7 @@ returns ENOSYS on the FUSE-backed volume mount):
488
512
  Persist scratch -> workspace (cp from /tmp into /workspace + Convex sync):
489
513
  dench stage <local-path> [<workspace-dest>] [--clean] [--json]
490
514
 
491
- Subagents (Daytona-native rewrite):
515
+ Subagents:
492
516
  dench agent spawn '<goal>' [--parent-run-id <id>] [--sandbox own|share_parent] [--time-budget-ms N]
493
517
  dench agent await --hook <token> | --children id1,id2,id3
494
518
  dench agent message <toRunId> '<text>' [--from-run-id <id>]
@@ -501,7 +525,7 @@ Subagents (Daytona-native rewrite):
501
525
 
502
526
  dench --version
503
527
 
504
- Self-updating agent harness (OpenClaw parity):
528
+ Self-updating agent harness:
505
529
  dench identity show | edit IDENTITY.md (org-wide)
506
530
  dench organisation show | edit ORGANISATION.md
507
531
  dench user show | edit USER.md (per-member)
@@ -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
 
@@ -2038,25 +2063,31 @@ function parseBillingTopupAmountUsd() {
2038
2063
  }
2039
2064
 
2040
2065
  async function billingStatus(runtime: Runtime) {
2041
- const sessionRuntime = requireSessionRuntime(runtime, "dench billing status");
2042
- const status = await sessionRuntime.client.query(
2066
+ const authedRuntime = requireAuthenticatedRuntime(
2067
+ runtime,
2068
+ "dench billing status",
2069
+ );
2070
+ const status = await authedRuntime.client.query(
2043
2071
  api.functions.agentWorkspace.getAgentBillingStatus,
2044
- { sessionToken: sessionRuntime.sessionToken },
2072
+ { sessionToken: authedRuntime.sessionToken },
2045
2073
  );
2046
2074
  print(json ? status : formatBillingStatus(status));
2047
2075
  }
2048
2076
 
2049
2077
  async function billingTopup(runtime: Runtime) {
2050
- const sessionRuntime = requireSessionRuntime(runtime, "dench billing topup");
2078
+ const authedRuntime = requireAuthenticatedRuntime(
2079
+ runtime,
2080
+ "dench billing topup",
2081
+ );
2051
2082
  const amountUsd = parseBillingTopupAmountUsd();
2052
2083
  const response = await fetch(
2053
- `${sessionRuntime.host.replace(/\/+$/, "")}/api/stripe/create-ai-topup`,
2084
+ `${authedRuntime.host.replace(/\/+$/, "")}/api/stripe/create-ai-topup`,
2054
2085
  {
2055
2086
  method: "POST",
2056
2087
  headers: {
2057
2088
  accept: "application/json",
2058
2089
  "content-type": "application/json",
2059
- authorization: `Bearer ${sessionRuntime.sessionToken}`,
2090
+ authorization: `Bearer ${authedRuntime.sessionToken}`,
2060
2091
  },
2061
2092
  body: JSON.stringify({
2062
2093
  amountUsd,
@@ -2123,7 +2154,7 @@ async function billingTopup(runtime: Runtime) {
2123
2154
  * needed in that case). We surface either outcome cleanly.
2124
2155
  */
2125
2156
  async function denchUpgrade(runtime: Runtime) {
2126
- const sessionRuntime = requireSessionRuntime(runtime, "dench upgrade");
2157
+ const authedRuntime = requireAuthenticatedRuntime(runtime, "dench upgrade");
2127
2158
  const tierFlag = option("--tier")?.trim().toLowerCase() || "pro";
2128
2159
  const cycleFlag =
2129
2160
  option("--cycle")?.trim().toLowerCase() ||
@@ -2146,13 +2177,13 @@ async function denchUpgrade(runtime: Runtime) {
2146
2177
  const internalTier = tierFlag === "pro" ? "desktop" : "cloud";
2147
2178
 
2148
2179
  const response = await fetch(
2149
- `${sessionRuntime.host.replace(/\/+$/, "")}/api/stripe/create-checkout`,
2180
+ `${authedRuntime.host.replace(/\/+$/, "")}/api/stripe/create-checkout`,
2150
2181
  {
2151
2182
  method: "POST",
2152
2183
  headers: {
2153
2184
  accept: "application/json",
2154
2185
  "content-type": "application/json",
2155
- authorization: `Bearer ${sessionRuntime.sessionToken}`,
2186
+ authorization: `Bearer ${authedRuntime.sessionToken}`,
2156
2187
  },
2157
2188
  body: JSON.stringify({
2158
2189
  tier: internalTier,
@@ -2394,25 +2425,6 @@ function stripRuntimeFlags(args: string[]) {
2394
2425
  return stripped;
2395
2426
  }
2396
2427
 
2397
- /**
2398
- * `session` mode is required for `agentWorkspace.*` calls because they
2399
- * gate on `validateAgentSession` server-side, which only accepts the
2400
- * `dch_agent_*` token format. Sandboxes that only have a unified
2401
- * `DENCH_API_KEY` (apiKey mode) cannot impersonate a specific agent
2402
- * and will fail at the server with `Invalid agent session` — surface
2403
- * that here as a clear error instead.
2404
- */
2405
- function requireSessionRuntime(
2406
- runtime: Runtime,
2407
- command: string,
2408
- ): Extract<Runtime, { mode: "session" }> {
2409
- if (runtime.mode === "session") return runtime;
2410
- if (runtime.mode === "apiKey") {
2411
- throw agentSessionRequiredError(command);
2412
- }
2413
- throw loginRequiredError(command);
2414
- }
2415
-
2416
2428
  /**
2417
2429
  * Org-scoped commands (`dench crm`, `dench files`, etc.) accept either
2418
2430
  * a `dch_agent_*` agent session token OR a unified Dench API key
@@ -2432,6 +2444,7 @@ function requireAuthenticatedRuntime(
2432
2444
  type GatewayCommandAuth = {
2433
2445
  bearerToken: string;
2434
2446
  gatewayBaseUrl?: string;
2447
+ gatewayHeaders?: Record<string, string>;
2435
2448
  };
2436
2449
 
2437
2450
  function gatewayBaseUrlForCli(value: unknown): string | undefined {
@@ -2528,6 +2541,12 @@ async function resolveGatewayCommandAuth(
2528
2541
  return {
2529
2542
  bearerToken: gatewayKey,
2530
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,
2531
2550
  };
2532
2551
  }
2533
2552
 
@@ -2543,36 +2562,6 @@ function encodeDevCrmSessionToken(args: {
2543
2562
  return `dch_dev_${encodeURIComponent(args.workspaceSlug)}:${encodeURIComponent(args.devKey)}`;
2544
2563
  }
2545
2564
 
2546
- function agentSessionRequiredError(command: string) {
2547
- return new CliError(
2548
- `${command} requires an agent session — DENCH_API_KEY is org-scoped and cannot impersonate a specific agent`,
2549
- {
2550
- code: "agent_session_required",
2551
- nextActions: [
2552
- 'Run dench signin --kind <kind> --name "AI Agent - Project" to mint an agent session for this command.',
2553
- "Or run the equivalent action via the workspace UI / dench crm if it is org-level (e.g. CRM data, files).",
2554
- ],
2555
- },
2556
- );
2557
- }
2558
-
2559
- /**
2560
- * `dench approval request` has separate session vs dev branches. With
2561
- * apiKey mode added, the dev branch would silently receive partial args.
2562
- * Use this to short-circuit that branch with a clear error.
2563
- *
2564
- * Asserts away the `apiKey` mode so the caller's downstream type
2565
- * narrowing (e.g. `runtime.workspaceArgs`) keeps working.
2566
- */
2567
- function rejectApiKeyForAgentCommand(
2568
- runtime: Runtime,
2569
- command: string,
2570
- ): asserts runtime is Exclude<Runtime, { mode: "apiKey" }> {
2571
- if (runtime.mode === "apiKey") {
2572
- throw agentSessionRequiredError(command);
2573
- }
2574
- }
2575
-
2576
2565
  function asRecord(value: unknown): JsonRecord | undefined {
2577
2566
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2578
2567
  return undefined;
@@ -2665,6 +2654,7 @@ async function connectedAppsContext(runtime: Runtime) {
2665
2654
  return fetchConnectedAppsSummary({
2666
2655
  bearerToken: gatewayAuth.bearerToken,
2667
2656
  gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
2657
+ gatewayHeaders: gatewayAuth.gatewayHeaders,
2668
2658
  });
2669
2659
  } catch (error) {
2670
2660
  return {
@@ -2679,7 +2669,7 @@ async function connectedAppsContext(runtime: Runtime) {
2679
2669
  async function buildContext(runtime: Runtime) {
2680
2670
  let mine: JsonRecord;
2681
2671
  let urls: JsonRecord;
2682
- if (runtime.mode === "session") {
2672
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
2683
2673
  mine = (await runtime.client.query(
2684
2674
  api.functions.agentWorkspace.whatCanIDoHere,
2685
2675
  { sessionToken: runtime.sessionToken },
@@ -2692,20 +2682,7 @@ async function buildContext(runtime: Runtime) {
2692
2682
  );
2693
2683
  urls = {};
2694
2684
  } else {
2695
- // apiKey mode: no agent session is available, so we can't call
2696
- // `whatCanIDoHere` (it gates on `validateAgentSession`). Return a
2697
- // minimal context derived from sandbox envVars so dench-cli help
2698
- // surfaces still work without a full login.
2699
- mine = {
2700
- authMode: "apiKey",
2701
- organization: runtime.organization ?? null,
2702
- runId: process.env.DENCH_RUN_ID?.trim() ?? null,
2703
- parentRunId: process.env.DENCH_PARENT_RUN_ID?.trim() ?? null,
2704
- rootRunId: process.env.DENCH_ROOT_RUN_ID?.trim() ?? null,
2705
- };
2706
- urls = runtime.host
2707
- ? workspaceUrls(runtime.host, runtime.organization?.slug)
2708
- : {};
2685
+ throw loginRequiredError("dench context");
2709
2686
  }
2710
2687
 
2711
2688
  return {
@@ -2933,7 +2910,7 @@ function summarizeDevMine(workspaceSlug: string, data: WorkspaceOverview) {
2933
2910
  }
2934
2911
 
2935
2912
  async function listArtifacts(runtime: Runtime, limit?: number) {
2936
- if (runtime.mode === "session") {
2913
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
2937
2914
  try {
2938
2915
  return (await runtime.client.query(
2939
2916
  api.functions.agentWorkspace.agentListArtifacts,
@@ -2951,12 +2928,6 @@ async function listArtifacts(runtime: Runtime, limit?: number) {
2951
2928
  );
2952
2929
  }
2953
2930
  }
2954
- if (runtime.mode === "apiKey") {
2955
- // No agent identity → no artifact list. Return empty so callers
2956
- // (e.g. `dench artifacts`, `dench suggested-work`) degrade gracefully
2957
- // instead of throwing for sandbox callers.
2958
- return [] as JsonRecord[];
2959
- }
2960
2931
  const data = await devOverview(runtime);
2961
2932
  return asRecordArray(data.recentArtifacts);
2962
2933
  }
@@ -3884,6 +3855,7 @@ async function main() {
3884
3855
  jsonOutput: json,
3885
3856
  bearerToken: gatewayAuth.bearerToken,
3886
3857
  gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
3858
+ gatewayHeaders: gatewayAuth.gatewayHeaders,
3887
3859
  });
3888
3860
  return;
3889
3861
  }
@@ -3903,6 +3875,7 @@ async function main() {
3903
3875
  jsonOutput: json,
3904
3876
  bearerToken: gatewayAuth.bearerToken,
3905
3877
  gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
3878
+ gatewayHeaders: gatewayAuth.gatewayHeaders,
3906
3879
  });
3907
3880
  return;
3908
3881
  }
@@ -4165,15 +4138,15 @@ async function main() {
4165
4138
  }
4166
4139
 
4167
4140
  if (command === "memory") {
4168
- const sessionRuntime = requireSessionRuntime(runtime, "dench memory");
4141
+ const authedRuntime = requireAuthenticatedRuntime(runtime, "dench memory");
4169
4142
  if (subcommand === "search") {
4170
4143
  const query = positionalsFrom(2).join(" ").trim();
4171
4144
  if (!query) throw new Error('Usage: dench memory search "query"');
4172
4145
  print(
4173
- await sessionRuntime.client.query(
4146
+ await authedRuntime.client.query(
4174
4147
  api.functions.agentWorkspace.agentSearchMemory,
4175
4148
  {
4176
- sessionToken: sessionRuntime.sessionToken,
4149
+ sessionToken: authedRuntime.sessionToken,
4177
4150
  query,
4178
4151
  limit: parseNumberOption("--limit"),
4179
4152
  },
@@ -4188,15 +4161,16 @@ async function main() {
4188
4161
  throw new Error('Usage: dench memory save <key> "text"');
4189
4162
  }
4190
4163
  print(
4191
- await sessionRuntime.client.mutation(
4164
+ await authedRuntime.client.mutation(
4192
4165
  api.functions.agentWorkspace.agentSaveMemory,
4193
4166
  {
4194
- sessionToken: sessionRuntime.sessionToken,
4167
+ sessionToken: authedRuntime.sessionToken,
4195
4168
  key,
4196
4169
  kind: (option("--kind") ?? "fact") as never,
4197
4170
  text,
4198
4171
  tags: parseCommaOption("--tags"),
4199
4172
  sensitivity: (option("--sensitivity") ?? "normal") as never,
4173
+ visibility: parseMemoryVisibilityOption() as never,
4200
4174
  },
4201
4175
  ),
4202
4176
  );
@@ -4216,9 +4190,9 @@ async function main() {
4216
4190
  }
4217
4191
 
4218
4192
  if (command === "status") {
4219
- if (runtime.mode === "session") {
4193
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
4220
4194
  const status = (await runtime.client.query(
4221
- api.functions.agentWorkspace.agentStatus,
4195
+ api.functions.agentWorkspace.whatCanIDoHere,
4222
4196
  {
4223
4197
  sessionToken: runtime.sessionToken,
4224
4198
  },
@@ -4230,13 +4204,6 @@ async function main() {
4230
4204
  print(status);
4231
4205
  return;
4232
4206
  }
4233
- if (runtime.mode === "apiKey") {
4234
- // No agent session → no agent-status query. Fall back to a thin
4235
- // env-derived snapshot so sandbox callers still get something
4236
- // useful from `dench status`.
4237
- print(await buildContext(runtime));
4238
- return;
4239
- }
4240
4207
  const data = await devOverview(runtime);
4241
4208
  const output = scopedStatus
4242
4209
  ? summarizeDevMine(runtime.workspaceArgs.workspaceSlug, data)
@@ -4248,9 +4215,9 @@ async function main() {
4248
4215
  }
4249
4216
 
4250
4217
  if (command === "agents") {
4251
- if (runtime.mode === "session") {
4218
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
4252
4219
  const status = await runtime.client.query(
4253
- api.functions.agentWorkspace.agentStatus,
4220
+ api.functions.agentWorkspace.whatCanIDoHere,
4254
4221
  {
4255
4222
  sessionToken: runtime.sessionToken,
4256
4223
  },
@@ -4258,18 +4225,15 @@ async function main() {
4258
4225
  print(status.agents);
4259
4226
  return;
4260
4227
  }
4261
- if (runtime.mode === "apiKey") {
4262
- throw loginRequiredError("dench agents");
4263
- }
4264
4228
  const data = await devOverview(runtime);
4265
4229
  print(data.agents);
4266
4230
  return;
4267
4231
  }
4268
4232
 
4269
4233
  if (command === "approvals") {
4270
- if (runtime.mode === "session") {
4234
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
4271
4235
  const status = await runtime.client.query(
4272
- api.functions.agentWorkspace.agentStatus,
4236
+ api.functions.agentWorkspace.whatCanIDoHere,
4273
4237
  {
4274
4238
  sessionToken: runtime.sessionToken,
4275
4239
  },
@@ -4277,9 +4241,6 @@ async function main() {
4277
4241
  print(status.approvals);
4278
4242
  return;
4279
4243
  }
4280
- if (runtime.mode === "apiKey") {
4281
- throw loginRequiredError("dench approvals");
4282
- }
4283
4244
  const data = await devOverview(runtime);
4284
4245
  print(data.approvals);
4285
4246
  return;
@@ -4309,7 +4270,7 @@ async function main() {
4309
4270
  if (command === "approval" && subcommand === "request") {
4310
4271
  const title = positional(2);
4311
4272
  if (!title) throw new Error("Missing approval title");
4312
- if (runtime.mode === "session") {
4273
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
4313
4274
  print(
4314
4275
  await runtime.client.mutation(
4315
4276
  api.functions.agentWorkspace.agentRequestApproval,
@@ -4323,7 +4284,6 @@ async function main() {
4323
4284
  );
4324
4285
  return;
4325
4286
  }
4326
- rejectApiKeyForAgentCommand(runtime, "dench approval request");
4327
4287
  const approvalId = await runtime.client.mutation(
4328
4288
  api.functions.agentWorkspace.devRequestApproval,
4329
4289
  {
@@ -4346,15 +4306,15 @@ async function main() {
4346
4306
  ) {
4347
4307
  const approvalId = positional(2);
4348
4308
  if (!approvalId) throw new Error("Missing approval id");
4349
- const sessionRuntime = requireSessionRuntime(
4309
+ const authedRuntime = requireAuthenticatedRuntime(
4350
4310
  runtime,
4351
4311
  "dench approval approve/reject",
4352
4312
  );
4353
4313
  const decision = subcommand === "approve" ? "approved" : "rejected";
4354
- const result = await sessionRuntime.client.mutation(
4314
+ const result = await authedRuntime.client.mutation(
4355
4315
  api.functions.agentWorkspace.agentDecideApproval,
4356
4316
  {
4357
- sessionToken: sessionRuntime.sessionToken,
4317
+ sessionToken: authedRuntime.sessionToken,
4358
4318
  approvalId: approvalId as never,
4359
4319
  decision,
4360
4320
  evidence: option("--evidence"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.0.3",
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);