@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 +33 -27
- package/crm.ts +60 -26
- package/dench.ts +245 -467
- package/image.ts +49 -40
- package/lib/enrichment-gateway.ts +2 -2
- package/package.json +1 -1
- package/search.ts +56 -48
- package/tools.ts +59 -34
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
task
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
101
|
-
using,
|
|
102
|
-
|
|
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
|
|
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
|
|
194
|
-
dench
|
|
199
|
+
dench signin --staging
|
|
200
|
+
dench signin --host https://workspace-staging.dench.com
|
|
195
201
|
```
|
|
196
202
|
|
|
197
|
-
## Sandbox / API key auth (no `dench
|
|
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
|
|
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
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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)
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1757
|
+
return enrichCompany(
|
|
1758
|
+
{
|
|
1759
|
+
domain: inputValue,
|
|
1760
|
+
requiredFields: args.target.requiredFields,
|
|
1761
|
+
},
|
|
1762
|
+
args.gateway,
|
|
1763
|
+
);
|
|
1738
1764
|
}
|
|
1739
|
-
return enrichPersonByIdentifier(
|
|
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 (
|
|
1813
|
+
if (
|
|
1814
|
+
error instanceof EnrichmentGatewayError ||
|
|
1815
|
+
error instanceof CrmCliError
|
|
1816
|
+
) {
|
|
1783
1817
|
return {
|
|
1784
1818
|
entryId: args.entry.id,
|
|
1785
1819
|
ok: false,
|