@dench.com/cli 2.0.0 → 2.0.2

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/README.md CHANGED
@@ -4,17 +4,21 @@ Agent-facing CLI for the Dench agent workspace.
4
4
 
5
5
  ## Usage
6
6
 
7
- Run without installing:
7
+ Run without installing. The npm package is `@dench.com/cli`, but the binary
8
+ it installs is named `dench`, so always invoke it as
9
+ `npx -y -p @dench.com/cli dench <command>`. A bare
10
+ `npx -y @dench.com/cli <command>` will fail because `npx` looks for a bin
11
+ matching the package's last path segment (`cli`) and there isn't one.
8
12
 
9
13
  ```bash
10
- npx -y @dench.com/cli login --name "AI Agent - Billing Repo"
14
+ npx -y -p @dench.com/cli dench signin --name "AI Agent - Billing Repo"
11
15
  ```
12
16
 
13
17
  Or install globally:
14
18
 
15
19
  ```bash
16
20
  npm install -g @dench.com/cli
17
- dench login --name "AI Agent - Billing Repo"
21
+ dench signin --name "AI Agent - Billing Repo"
18
22
  ```
19
23
 
20
24
  `--kind` accepts any string. Suggested values: `claude_code`, `codex`, `cursor`,
@@ -47,11 +51,13 @@ DENCH_HOST=https://example.com dench <cmd>
47
51
 
48
52
  The smooth setup flow is:
49
53
 
50
- 1. Run `dench login --name "AI Agent - Billing Repo"` once.
54
+ 1. Run `dench signin --name "AI Agent - Billing Repo"` once.
51
55
  2. Let the CLI open the approval link. If it cannot, ask the human to open the
52
56
  printed link and confirm the email/workspace. Use `--no-open` to print the
53
57
  link without opening it.
54
- 3. Run `dench status --mine --json` and `dench tasks --json` to verify setup.
58
+ 3. Run `dench status --mine --json` and `dench crm entries list task --json`
59
+ to verify setup. Tasks live as CRM `task` entries — there is no separate
60
+ `dench tasks` command.
55
61
 
56
62
  The CLI selects the approved session for later commands automatically.
57
63
  Setup is complete after approval, both verification commands work, and you know
@@ -71,11 +77,11 @@ dench sessions
71
77
  dench use <session-key-or-workspace-slug>
72
78
  ```
73
79
 
74
- Use a human-readable `DENCH_SESSION_KEY` at login time only for long-lived
80
+ Use a human-readable `DENCH_SESSION_KEY` at sign-in time only for long-lived
75
81
  agents or when the human asks for a stable identity:
76
82
 
77
83
  ```bash
78
- DENCH_SESSION_KEY=billing-repo-agent dench login --name "AI Agent - Billing Repo"
84
+ DENCH_SESSION_KEY=billing-repo-agent dench signin --name "AI Agent - Billing Repo"
79
85
  DENCH_SESSION_KEY=billing-repo-agent dench status --mine --json
80
86
  ```
81
87
 
@@ -88,18 +94,18 @@ context. Other saved sessions can still be reused by future chats. To remove the
88
94
  wrong local session, run `dench logout --session <session-key>`. Use
89
95
  `dench logout --all` only when you want to clear every local Dench session.
90
96
 
91
- Do not create or edit local agent memory files during setup. Use Dench logs for
92
- meaningful progress, and use Dench memory only when the human asks or when
93
- saving a stable workspace fact.
97
+ Do not create or edit local agent memory files during setup. Use Dench
98
+ memory only when the human asks or when saving a stable workspace fact.
94
99
 
95
- Do not create, claim, or log a setup task by default. Only create or claim a
96
- task if the human assigns work, an open matching task exists, or coordination
97
- benefits from creating one. For ad-hoc requests, work directly and only log
98
- meaningful work.
100
+ Tasks live as CRM `task` entries there is no separate `dench tasks`,
101
+ `dench task`, `dench claim`, or `dench log` command. Create one via
102
+ `dench crm entries create task --cells '{"Name":"…","Status":"Open"}'`
103
+ only when the human assigns durable work or coordination benefits from
104
+ a tracked entry. For ad-hoc requests, work directly.
99
105
 
100
- Use `dench context` when you need to know who you are, which workspace you are
101
- using, assigned tasks, pending approvals you requested, connected apps, and the
102
- best next commands. Add `--json` for structured output.
106
+ Use `dench context` when you need to know who you are, which workspace you
107
+ are using, pending approvals you requested, connected apps, and the best
108
+ next commands. Add `--json` for structured output.
103
109
 
104
110
  ## Past chat threads
105
111
 
@@ -123,7 +129,7 @@ text — pick concrete tokens that the prior chat would have used (file
123
129
  paths, product names, error strings) rather than paraphrases.
124
130
 
125
131
  Inside a sandbox, `dench chat` uses `DENCH_API_KEY` + `DENCH_RUN_ID`
126
- automatically; locally, run `dench login` first.
132
+ automatically; locally, run `dench signin` first.
127
133
 
128
134
  ## Billing
129
135
 
@@ -190,16 +196,16 @@ Run `dench tool --help` or `dench apps --help` for command help.
190
196
  For staging:
191
197
 
192
198
  ```bash
193
- dench login --staging
194
- dench login --host https://workspace-staging.dench.com
199
+ dench signin --staging
200
+ dench signin --host https://workspace-staging.dench.com
195
201
  ```
196
202
 
197
- ## Sandbox / API key auth (no `dench login` required)
203
+ ## Sandbox / API key auth (no `dench signin` required)
198
204
 
199
205
  Inside a Daytona sandbox the unified Dench API key is baked into envVars
200
206
  at create time (see `src/lib/secrets/sandbox-tokens.ts`). When
201
207
  `DENCH_API_KEY` and `CONVEX_URL` are both present, the CLI skips the
202
- `dench login` flow entirely and uses the API key as a bearer for every
208
+ `dench signin` flow entirely and uses the API key as a bearer for every
203
209
  org-scoped command — `dench crm`, `dench files`, `dench fs sync`, etc.
204
210
 
205
211
  ```bash
@@ -217,7 +223,7 @@ Auth precedence:
217
223
 
218
224
  1. Explicit `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
219
225
  `DENCH_WORKSPACE`, `DENCH_DEV_AGENT_KEY`).
220
- 2. A locally-stored `dench login` session for the current backend (set via
226
+ 2. A locally-stored `dench signin` session for the current backend (set via
221
227
  `dench backend`, `--host`, `--prod`, or `--staging`).
222
228
  3. `DENCH_API_KEY` + `CONVEX_URL` (sandbox / automation path).
223
229
 
@@ -243,7 +249,7 @@ Auto-load now turns on only when one of:
243
249
  Explicit shell exports (`GATEWAY_URL=… ./dench …`) always win regardless.
244
250
 
245
251
  Commands that need to impersonate a specific agent (`dench memory`,
246
- `dench task`, `dench claim`, `dench log`, `dench approval request`,
247
- etc.) still require an agent session (`dench login`) — the unified API
248
- key is org-scoped and cannot speak as a single agent. Those commands
249
- return a clear "agent session required" error in API-key mode.
252
+ `dench approval request`, etc.) still require an agent session
253
+ (`dench signin`) — the unified API key is org-scoped and cannot speak
254
+ as a single agent. Those commands return a clear "agent session
255
+ required" error in API-key mode.
package/crm.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * crm docs list / create / link
27
27
  * crm actions list / run / runs
28
28
  */
29
- import { ConvexHttpClient } from "convex/browser";
29
+ import type { ConvexHttpClient } from "convex/browser";
30
30
  import { makeFunctionReference } from "convex/server";
31
31
  import {
32
32
  assertNoUnknownFlags as assertNoUnknownFlagsRaw,
@@ -40,18 +40,19 @@ import {
40
40
  import {
41
41
  autoDetectInputField,
42
42
  detectEnrichmentCategory,
43
- extractEnrichmentValue,
44
- findEnrichmentColumn,
45
- getRequiredFieldsForApolloPath,
46
43
  type EnrichmentCategory,
47
44
  type EnrichmentColumnDef,
45
+ extractEnrichmentValue,
48
46
  type FieldCandidate,
47
+ findEnrichmentColumn,
48
+ getRequiredFieldsForApolloPath,
49
49
  } from "./lib/crm-enrichment";
50
50
  import {
51
51
  aviatoEnrichCompany,
52
52
  aviatoEnrichPerson,
53
- enrichCompany,
54
53
  EnrichmentGatewayError,
54
+ type EnrichmentGatewayOptions,
55
+ enrichCompany,
55
56
  enrichPersonByIdentifier,
56
57
  } from "./lib/enrichment-gateway";
57
58
 
@@ -82,6 +83,7 @@ type CrmCliContext = {
82
83
  * organization context.
83
84
  */
84
85
  sessionToken?: string;
86
+ enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
85
87
  };
86
88
 
87
89
  // Adapt the shared helpers to throw CrmCliError so existing call sites
@@ -310,12 +312,14 @@ function out(ctx: CrmCliContext, value: unknown): void {
310
312
  console.log(JSON.stringify(value, null, 2));
311
313
  }
312
314
 
313
- function getFieldEnrichmentFlag(args: string[]): {
314
- category: EnrichmentCategory;
315
- key: string;
316
- apolloPath: string;
317
- inputFieldName: string;
318
- } | undefined {
315
+ function getFieldEnrichmentFlag(args: string[]):
316
+ | {
317
+ category: EnrichmentCategory;
318
+ key: string;
319
+ apolloPath: string;
320
+ inputFieldName: string;
321
+ }
322
+ | undefined {
319
323
  const category = parseCategoryFlag(getFlag(args, "--enrichment-category"));
320
324
  const key = getFlag(args, "--enrichment-key");
321
325
  const apolloPath = getFlag(args, "--apollo-path");
@@ -431,6 +435,7 @@ export async function runCrmCommand(opts: {
431
435
  convex: ConvexHttpClient;
432
436
  args: string[];
433
437
  sessionToken?: string;
438
+ enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
434
439
  }): Promise<void> {
435
440
  const args = [...opts.args];
436
441
  const jsonOutput = hasFlag(args, "--json");
@@ -439,6 +444,7 @@ export async function runCrmCommand(opts: {
439
444
  args,
440
445
  jsonOutput,
441
446
  sessionToken: opts.sessionToken,
447
+ enrichmentGateway: opts.enrichmentGateway,
442
448
  };
443
449
  const subcommand = args.shift();
444
450
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -1531,13 +1537,18 @@ type ObjectEnrichmentArgs = Omit<CellEnrichmentArgs, "entryId"> & {
1531
1537
  limit?: number;
1532
1538
  };
1533
1539
 
1534
- function parseCategoryFlag(value: string | undefined): EnrichmentCategory | undefined {
1540
+ function parseCategoryFlag(
1541
+ value: string | undefined,
1542
+ ): EnrichmentCategory | undefined {
1535
1543
  if (value === undefined) return undefined;
1536
1544
  if (value === "people" || value === "company") return value;
1537
1545
  throw new CrmCliError("--category must be people or company");
1538
1546
  }
1539
1547
 
1540
- function parsePositiveInt(flag: string, value: string | undefined): number | undefined {
1548
+ function parsePositiveInt(
1549
+ flag: string,
1550
+ value: string | undefined,
1551
+ ): number | undefined {
1541
1552
  if (value === undefined) return undefined;
1542
1553
  const parsed = Number(value);
1543
1554
  if (!Number.isInteger(parsed) || parsed < 1) {
@@ -1556,7 +1567,9 @@ function asFields(value: unknown): CrmFieldForEnrichment[] {
1556
1567
  .filter((field): field is CrmFieldForEnrichment => {
1557
1568
  if (!field || typeof field !== "object") return false;
1558
1569
  const candidate = field as Record<string, unknown>;
1559
- return typeof candidate.name === "string" && typeof candidate.type === "string";
1570
+ return (
1571
+ typeof candidate.name === "string" && typeof candidate.type === "string"
1572
+ );
1560
1573
  })
1561
1574
  .map((field) => ({
1562
1575
  id: typeof field.id === "string" ? field.id : undefined,
@@ -1601,7 +1614,9 @@ function asEntry(value: unknown): CrmEntryForEnrichment | null {
1601
1614
 
1602
1615
  function asEntries(value: unknown): CrmEntryForEnrichment[] {
1603
1616
  if (!Array.isArray(value)) return [];
1604
- return value.map(asEntry).filter((entry): entry is CrmEntryForEnrichment => !!entry);
1617
+ return value
1618
+ .map(asEntry)
1619
+ .filter((entry): entry is CrmEntryForEnrichment => !!entry);
1605
1620
  }
1606
1621
 
1607
1622
  function resolveEnrichmentColumn(args: {
@@ -1668,7 +1683,8 @@ async function resolveEnrichmentTarget(
1668
1683
  apolloPath: args.apolloPath ?? outputField.enrichment?.apolloPath,
1669
1684
  });
1670
1685
 
1671
- const inputFieldName = args.inputFieldName ?? outputField.enrichment?.inputFieldName;
1686
+ const inputFieldName =
1687
+ args.inputFieldName ?? outputField.enrichment?.inputFieldName;
1672
1688
  const inputField = inputFieldName
1673
1689
  ? fields.find((field) => field.name === inputFieldName)
1674
1690
  : autoDetectInputField(category, fields);
@@ -1693,6 +1709,7 @@ async function callEnrichmentGatewayForEntry(args: {
1693
1709
  entry: CrmEntryForEnrichment;
1694
1710
  /** Explicit provider override. Defaults: people=fullenrich, company=aviato. */
1695
1711
  provider?: string;
1712
+ gateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
1696
1713
  }): Promise<Record<string, unknown>> {
1697
1714
  const input = args.entry.fields[args.target.inputField.name];
1698
1715
  if (!hasCellValue(input)) {
@@ -1702,7 +1719,8 @@ async function callEnrichmentGatewayForEntry(args: {
1702
1719
  }
1703
1720
  const inputValue = String(input);
1704
1721
  const isCompany = args.target.category === "company";
1705
- const resolvedProvider = args.provider ?? (isCompany ? "aviato" : "fullenrich");
1722
+ const resolvedProvider =
1723
+ args.provider ?? (isCompany ? "aviato" : "fullenrich");
1706
1724
 
1707
1725
  if (resolvedProvider === "aviato") {
1708
1726
  // Guard: Aviato does not return phone or email.
@@ -1718,25 +1736,37 @@ async function callEnrichmentGatewayForEntry(args: {
1718
1736
  const isLinkedin = /linkedin\.com/i.test(inputValue);
1719
1737
  return aviatoEnrichCompany(
1720
1738
  isLinkedin ? { linkedinUrl: inputValue } : { website: inputValue },
1739
+ args.gateway,
1721
1740
  );
1722
1741
  } else {
1723
1742
  // For people, inputValue may be a LinkedIn URL, email, or LinkedIn ID
1724
1743
  const isLinkedin = /linkedin\.com/i.test(inputValue);
1725
1744
  const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inputValue);
1726
- if (isLinkedin) return aviatoEnrichPerson({ linkedinUrl: inputValue });
1727
- if (isEmail) return aviatoEnrichPerson({ email: inputValue });
1728
- return aviatoEnrichPerson({ linkedinID: inputValue });
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);
1729
1752
  }
1730
1753
  }
1731
1754
 
1732
1755
  // fullenrich path (default for people, explicit override for companies)
1733
1756
  if (isCompany) {
1734
- return enrichCompany({
1735
- domain: inputValue,
1736
- requiredFields: args.target.requiredFields,
1737
- });
1757
+ return enrichCompany(
1758
+ {
1759
+ domain: inputValue,
1760
+ requiredFields: args.target.requiredFields,
1761
+ },
1762
+ args.gateway,
1763
+ );
1738
1764
  }
1739
- return enrichPersonByIdentifier(inputValue, args.target.requiredFields);
1765
+ return enrichPersonByIdentifier(
1766
+ inputValue,
1767
+ args.target.requiredFields,
1768
+ args.gateway,
1769
+ );
1740
1770
  }
1741
1771
 
1742
1772
  async function enrichOneEntry(
@@ -1751,6 +1781,7 @@ async function enrichOneEntry(
1751
1781
  target: args.target,
1752
1782
  entry: args.entry,
1753
1783
  provider: args.provider,
1784
+ gateway: ctx.enrichmentGateway,
1754
1785
  });
1755
1786
  const value = extractEnrichmentValue(payload, args.target.column);
1756
1787
  if (value === null) {
@@ -1779,7 +1810,10 @@ async function enrichOneEntry(
1779
1810
  ...writeResult,
1780
1811
  };
1781
1812
  } catch (error) {
1782
- if (error instanceof EnrichmentGatewayError || error instanceof CrmCliError) {
1813
+ if (
1814
+ error instanceof EnrichmentGatewayError ||
1815
+ error instanceof CrmCliError
1816
+ ) {
1783
1817
  return {
1784
1818
  entryId: args.entry.id,
1785
1819
  ok: false,