@dench.com/cli 0.4.9 → 2.0.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/README.md +1 -1
- package/dench.ts +662 -171
- package/host.ts +3 -3
- package/lib/enrichment-gateway.ts +28 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ dench login --name "AI Agent - Billing Repo"
|
|
|
23
23
|
to lowercase snake_case before send (e.g. `--kind "Claude Code"` becomes
|
|
24
24
|
`claude_code`).
|
|
25
25
|
|
|
26
|
-
The default backend is `https://dench.
|
|
26
|
+
The default backend is `https://dench.com` (production). Use `dench backend`
|
|
27
27
|
to switch:
|
|
28
28
|
|
|
29
29
|
```bash
|
package/dench.ts
CHANGED
|
@@ -73,7 +73,7 @@ type ConfigFile = {
|
|
|
73
73
|
* Explicit user choice of which Dench backend to talk to by default,
|
|
74
74
|
* set via `dench backend set <local|staging|prod|<url>>`. Takes
|
|
75
75
|
* precedence over the legacy `currentHost` field (which used to be
|
|
76
|
-
* silently updated by every `dench
|
|
76
|
+
* silently updated by every `dench signin` and caused the CLI to
|
|
77
77
|
* silently follow the last-logged-in environment).
|
|
78
78
|
*
|
|
79
79
|
* Resolution order in `resolveHost`:
|
|
@@ -164,8 +164,8 @@ const api = {
|
|
|
164
164
|
agentUpdateTaskStatus: makeFunctionReference<"mutation">(
|
|
165
165
|
"functions/agentWorkspace:agentUpdateTaskStatus",
|
|
166
166
|
),
|
|
167
|
-
|
|
168
|
-
"functions/agentWorkspace:
|
|
167
|
+
createSignInRequest: makeFunctionReference<"mutation">(
|
|
168
|
+
"functions/agentWorkspace:createSignInRequest",
|
|
169
169
|
),
|
|
170
170
|
devAppendLog: makeFunctionReference<"mutation">(
|
|
171
171
|
"functions/agentWorkspace:devAppendLog",
|
|
@@ -182,14 +182,11 @@ const api = {
|
|
|
182
182
|
devListWorkspaceOverview: makeFunctionReference<"query">(
|
|
183
183
|
"functions/agentWorkspace:devListWorkspaceOverview",
|
|
184
184
|
),
|
|
185
|
-
devRegisterAgent: makeFunctionReference<"mutation">(
|
|
186
|
-
"functions/agentWorkspace:devRegisterAgent",
|
|
187
|
-
),
|
|
188
185
|
devRequestApproval: makeFunctionReference<"mutation">(
|
|
189
186
|
"functions/agentWorkspace:devRequestApproval",
|
|
190
187
|
),
|
|
191
|
-
|
|
192
|
-
"functions/agentWorkspace:
|
|
188
|
+
pollSignInRequest: makeFunctionReference<"mutation">(
|
|
189
|
+
"functions/agentWorkspace:pollSignInRequest",
|
|
193
190
|
),
|
|
194
191
|
whatCanIDoHere: makeFunctionReference<"query">(
|
|
195
192
|
"functions/agentWorkspace:whatCanIDoHere",
|
|
@@ -336,7 +333,7 @@ function normalizeCliError(error: unknown): CliError {
|
|
|
336
333
|
nextActions: [
|
|
337
334
|
"Check that DENCH_DEV_AGENT_KEY in your shell matches the Convex deployment env.",
|
|
338
335
|
"Use --dev only for a local/dev Convex deployment configured with that key.",
|
|
339
|
-
"For normal terminal usage, run dench
|
|
336
|
+
"For normal terminal usage, run dench signin or set DENCH_API_KEY + CONVEX_URL.",
|
|
340
337
|
],
|
|
341
338
|
});
|
|
342
339
|
}
|
|
@@ -344,7 +341,7 @@ function normalizeCliError(error: unknown): CliError {
|
|
|
344
341
|
return new CliError("Dev CRM access is disabled in production", {
|
|
345
342
|
code: "dev_access_disabled",
|
|
346
343
|
nextActions: [
|
|
347
|
-
"Remove --dev and run dench
|
|
344
|
+
"Remove --dev and run dench signin for terminal access.",
|
|
348
345
|
"Or use DENCH_API_KEY + CONVEX_URL for sandbox/automation access.",
|
|
349
346
|
],
|
|
350
347
|
});
|
|
@@ -354,8 +351,8 @@ function normalizeCliError(error: unknown): CliError {
|
|
|
354
351
|
code: "invalid_api_key",
|
|
355
352
|
nextActions: [
|
|
356
353
|
"Check DENCH_API_KEY and CONVEX_URL point to the same organization/deployment.",
|
|
357
|
-
"Unset DENCH_API_KEY to use a saved dench
|
|
358
|
-
"Or run dench
|
|
354
|
+
"Unset DENCH_API_KEY to use a saved dench signin session instead.",
|
|
355
|
+
"Or run dench signin to create a new terminal session.",
|
|
359
356
|
],
|
|
360
357
|
});
|
|
361
358
|
}
|
|
@@ -423,10 +420,11 @@ function help() {
|
|
|
423
420
|
console.log(`Dench CLI
|
|
424
421
|
|
|
425
422
|
Usage:
|
|
426
|
-
dench
|
|
427
|
-
dench
|
|
428
|
-
dench
|
|
429
|
-
dench
|
|
423
|
+
dench signin [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
|
|
424
|
+
dench signin --new-workspace --org-name "Workspace" [--kind <kind>] [--name "AI Agent - Project"]
|
|
425
|
+
dench signin --email user@example.com [--new-workspace --org-name "Workspace"] [--kind <kind>] [--name "AI Agent - Project"]
|
|
426
|
+
dench signin --org <workspace-slug> [--kind <kind>] [--name "AI Agent - Project"]
|
|
427
|
+
dench upgrade [--tier pro|max] [--cycle monthly|yearly] [--no-open] [--json]
|
|
430
428
|
dench sessions [--host <host>] [--json]
|
|
431
429
|
dench use <session-key-or-workspace-slug> [--host <host>] [--json]
|
|
432
430
|
dench logout [session-key-or-workspace-slug] [--session <key-or-scope>] [--host <host>] [--all] [--json]
|
|
@@ -570,14 +568,18 @@ Billing:
|
|
|
570
568
|
dench billing topup --amount 5 creates a Stripe Checkout link for the human to pay.
|
|
571
569
|
|
|
572
570
|
Agent setup:
|
|
573
|
-
dench
|
|
574
|
-
|
|
571
|
+
dench signin is the only auth surface. Two transports (browser link
|
|
572
|
+
by default, OTP paste-back with --email <addr>) cover both intents:
|
|
573
|
+
signing in to an existing workspace or creating a new free CRM-only
|
|
574
|
+
workspace (--new-workspace --org-name "<name>"). New workspaces have
|
|
575
|
+
full CRM access; run \`dench upgrade\` to unlock chat, file sync,
|
|
576
|
+
enrichment, and image generation.
|
|
575
577
|
--kind accepts any string. Suggested values: claude_code, codex, cursor,
|
|
576
578
|
hermes, openclaw, or any custom kind (e.g. aider, goose, internal_orchestrator).
|
|
577
579
|
Defaults to "other" when omitted. Values are normalized to lowercase
|
|
578
580
|
snake_case before send (e.g. "Claude Code" -> claude_code).
|
|
579
|
-
After
|
|
580
|
-
|
|
581
|
+
After sign-in, run dench context --json and dench crm objects list --json
|
|
582
|
+
to verify the workspace + CRM are wired up.
|
|
581
583
|
Do not create, claim, or log a setup task by default.
|
|
582
584
|
|
|
583
585
|
Backends and hosts:
|
|
@@ -602,7 +604,7 @@ Dev fallback (run against a local Convex deployment with a dev key):
|
|
|
602
604
|
|
|
603
605
|
Advanced:
|
|
604
606
|
For long-lived agents or when the human asks:
|
|
605
|
-
DENCH_SESSION_KEY=stable-agent-id dench
|
|
607
|
+
DENCH_SESSION_KEY=stable-agent-id dench signin
|
|
606
608
|
DENCH_SESSION_KEY=stable-agent-id dench logout
|
|
607
609
|
DENCH_SESSION_KEY=stable-agent-id dench status
|
|
608
610
|
`);
|
|
@@ -621,7 +623,7 @@ Usage:
|
|
|
621
623
|
|
|
622
624
|
Auth:
|
|
623
625
|
All commands authenticate with DENCH_API_KEY (Bearer) directly to the
|
|
624
|
-
Dench Cloud Gateway. No \`dench
|
|
626
|
+
Dench Cloud Gateway. No \`dench signin\` agent session is required —
|
|
625
627
|
inside a Dench sandbox the key is baked into the env automatically;
|
|
626
628
|
outside, export DENCH_API_KEY=<key> first.
|
|
627
629
|
|
|
@@ -669,39 +671,46 @@ be converted into a workspace task.
|
|
|
669
671
|
`);
|
|
670
672
|
}
|
|
671
673
|
|
|
672
|
-
function
|
|
673
|
-
console.log(`Dench
|
|
674
|
-
|
|
675
|
-
Usage:
|
|
676
|
-
dench onboard [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
|
|
677
|
-
dench setup [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
|
|
678
|
-
dench what-can-i-do [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
|
|
679
|
-
|
|
680
|
-
Logs in if needed, then shows what this agent can do here: workspace rules,
|
|
681
|
-
open work, suggested work, connected-tool commands, memory, approvals, and
|
|
682
|
-
next actions.
|
|
683
|
-
`);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function loginHelp() {
|
|
687
|
-
console.log(`Dench login
|
|
674
|
+
function signinHelp() {
|
|
675
|
+
console.log(`Dench signin
|
|
688
676
|
|
|
689
677
|
Usage:
|
|
690
|
-
dench
|
|
691
|
-
|
|
692
|
-
Agent
|
|
693
|
-
--kind
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
678
|
+
dench signin [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
|
|
679
|
+
dench signin --new-workspace --org-name "Workspace Name" [--kind <kind>] [--name "AI Agent - Project"]
|
|
680
|
+
dench signin --email user@example.com [--new-workspace --org-name "Workspace Name"] [--kind <kind>] [--name "AI Agent - Project"]
|
|
681
|
+
dench signin --org <workspace-slug> [--kind <kind>] [--name "AI Agent - Project"]
|
|
682
|
+
|
|
683
|
+
Signs in a CLI agent to a Dench workspace. Two transport modes:
|
|
684
|
+
|
|
685
|
+
Browser (default): opens a one-click approval URL. The human picks
|
|
686
|
+
or creates a workspace in the browser, then the CLI saves the
|
|
687
|
+
session token. Use --no-open to print the link instead.
|
|
688
|
+
|
|
689
|
+
OTP (with --email): prompts for the 6-digit code the user receives
|
|
690
|
+
by email; never opens a browser. Useful for headless / remote
|
|
691
|
+
shells.
|
|
692
|
+
|
|
693
|
+
Intents:
|
|
694
|
+
|
|
695
|
+
Sign in to existing workspace (default): the user picks one of
|
|
696
|
+
their admin workspaces in the browser. With OTP, the CLI prompts
|
|
697
|
+
when more than one admin workspace exists.
|
|
698
|
+
|
|
699
|
+
--new-workspace --org-name "<name>": creates a new FREE CRM-only
|
|
700
|
+
workspace with that name. Free workspaces have full CRM access;
|
|
701
|
+
chat, file sync, enrichment, and image-gen are paywalled — run
|
|
702
|
+
\`dench upgrade\` to unlock those.
|
|
703
|
+
|
|
704
|
+
Agent identity:
|
|
705
|
+
--kind accepts any string. Suggested values: claude_code, codex,
|
|
706
|
+
cursor, hermes, openclaw, or any custom kind. Values are
|
|
707
|
+
normalized to lowercase snake_case (e.g. "Claude Code" \u2192
|
|
708
|
+
claude_code). Defaults to "other".
|
|
709
|
+
--name is a human-friendly label, e.g. "Cursor Agent - Billing Repo".
|
|
710
|
+
|
|
711
|
+
After sign-in, run \`dench context --json\` to confirm the active
|
|
712
|
+
workspace and agent, then \`dench crm objects list --json\` to verify
|
|
713
|
+
CRM access.
|
|
705
714
|
`);
|
|
706
715
|
}
|
|
707
716
|
|
|
@@ -753,7 +762,7 @@ Usage:
|
|
|
753
762
|
|
|
754
763
|
Lists all external apps connected to this organization (Slack, Gmail,
|
|
755
764
|
GitHub, etc.). Authenticates with DENCH_API_KEY (Bearer) directly to the
|
|
756
|
-
Dench Cloud Gateway — no \`dench
|
|
765
|
+
Dench Cloud Gateway — no \`dench signin\` agent session required. Alias
|
|
757
766
|
for \`dench tool status\`.
|
|
758
767
|
`);
|
|
759
768
|
}
|
|
@@ -786,7 +795,7 @@ What this does:
|
|
|
786
795
|
The default Dench backend is production (${PRODUCTION_HOST}). "dench backend set"
|
|
787
796
|
stores an explicit override in ~/.dench/config.json so every future
|
|
788
797
|
"dench" invocation talks to that backend by default. Use it instead of
|
|
789
|
-
relying on a stale currentHost from a past "dench
|
|
798
|
+
relying on a stale currentHost from a past "dench signin".
|
|
790
799
|
|
|
791
800
|
Setting the backend to "local" also opts the CLI into auto-loading
|
|
792
801
|
.env and .env.local from the dench.com workspace at startup, which is
|
|
@@ -810,7 +819,7 @@ function resolveHost(config: ConfigFile | undefined, _scope: SessionScope) {
|
|
|
810
819
|
// represents an explicit user choice. Production wins on fresh
|
|
811
820
|
// installs (no flags, no env, no config) — which is the safe default
|
|
812
821
|
// for an agent CLI. The per-scope `currentHosts[scope.key]` map is
|
|
813
|
-
// intentionally NOT consulted here so that a past `dench
|
|
822
|
+
// intentionally NOT consulted here so that a past `dench signin
|
|
814
823
|
// --host http://localhost:3000` doesn't silently keep routing every
|
|
815
824
|
// future command at local dev.
|
|
816
825
|
return resolveHostFromArgs(filteredArgs, config?.defaultBackend);
|
|
@@ -854,19 +863,6 @@ function workspaceLabel(session: StoredSession) {
|
|
|
854
863
|
: session.organization.name;
|
|
855
864
|
}
|
|
856
865
|
|
|
857
|
-
function loginNextCommandsMessage(
|
|
858
|
-
session: StoredSession,
|
|
859
|
-
stableSessionKey: string | undefined,
|
|
860
|
-
) {
|
|
861
|
-
const workspaceName = session.organization?.name ?? "unknown workspace";
|
|
862
|
-
const workspaceSlug = session.organization?.slug ?? "unknown";
|
|
863
|
-
const agentName = session.agent?.name ?? "unknown agent";
|
|
864
|
-
const stableKeyText = stableSessionKey
|
|
865
|
-
? ` Stable session key: ${stableSessionKey}.`
|
|
866
|
-
: "";
|
|
867
|
-
return `Next commands will use workspace ${workspaceName} (${workspaceSlug}) as ${agentName}.${stableKeyText}`;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
866
|
function ambiguousSessionMessage(
|
|
871
867
|
config: ConfigFile,
|
|
872
868
|
scope: SessionScope,
|
|
@@ -1539,29 +1535,184 @@ async function discoverConvexUrl(host: string) {
|
|
|
1539
1535
|
return payload.convexUrl;
|
|
1540
1536
|
}
|
|
1541
1537
|
|
|
1542
|
-
|
|
1538
|
+
/**
|
|
1539
|
+
* Read a single line from stdin. Used by `dench signin --email` for
|
|
1540
|
+
* the OTP code and (when the user has multiple admin workspaces) the
|
|
1541
|
+
* org-picker prompt. Returns the trimmed line or throws if stdin
|
|
1542
|
+
* isn't a TTY — callers can pre-supply the value via flags to avoid
|
|
1543
|
+
* the prompt in CI / non-interactive shells.
|
|
1544
|
+
*/
|
|
1545
|
+
async function promptLine(question: string): Promise<string> {
|
|
1546
|
+
if (!process.stdin.isTTY) {
|
|
1547
|
+
throw new CliError(
|
|
1548
|
+
`Non-interactive shell: pass the answer to "${question}" via a CLI flag instead of prompting.`,
|
|
1549
|
+
{ code: "stdin_not_tty" },
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
process.stdout.write(question);
|
|
1553
|
+
return await new Promise<string>((resolve, reject) => {
|
|
1554
|
+
let data = "";
|
|
1555
|
+
const onData = (chunk: Buffer | string) => {
|
|
1556
|
+
data += chunk.toString();
|
|
1557
|
+
if (data.includes("\n")) {
|
|
1558
|
+
cleanup();
|
|
1559
|
+
resolve(data.trim());
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
const onErr = (err: Error) => {
|
|
1563
|
+
cleanup();
|
|
1564
|
+
reject(err);
|
|
1565
|
+
};
|
|
1566
|
+
const cleanup = () => {
|
|
1567
|
+
process.stdin.removeListener("data", onData);
|
|
1568
|
+
process.stdin.removeListener("error", onErr);
|
|
1569
|
+
process.stdin.pause();
|
|
1570
|
+
};
|
|
1571
|
+
process.stdin.resume();
|
|
1572
|
+
process.stdin.on("data", onData);
|
|
1573
|
+
process.stdin.on("error", onErr);
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
type SignInIntent = "join_existing" | "create_workspace";
|
|
1578
|
+
|
|
1579
|
+
function signinNextCommandsMessage(
|
|
1580
|
+
session: StoredSession,
|
|
1581
|
+
stableSessionKey: string | undefined,
|
|
1582
|
+
) {
|
|
1583
|
+
const workspaceName = session.organization?.name ?? "unknown workspace";
|
|
1584
|
+
const workspaceSlug = session.organization?.slug ?? "unknown";
|
|
1585
|
+
const agentName = session.agent?.name ?? "unknown agent";
|
|
1586
|
+
const stableKeyText = stableSessionKey
|
|
1587
|
+
? ` Stable session key: ${stableSessionKey}.`
|
|
1588
|
+
: "";
|
|
1589
|
+
return `Signed in. Next commands will use workspace ${workspaceName} (${workspaceSlug}) as ${agentName}.${stableKeyText}`;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* `dench signin` — the single unified CLI auth entry point.
|
|
1594
|
+
*
|
|
1595
|
+
* Two transports (browser link or OTP) × two intents (join existing
|
|
1596
|
+
* workspace or create a new free CRM-only workspace). Replaces the
|
|
1597
|
+
* deprecated `dench login`, `dench onboard`, `dench setup`,
|
|
1598
|
+
* `dench what-can-i-do`, and `dench register` commands.
|
|
1599
|
+
*
|
|
1600
|
+
* Flag matrix:
|
|
1601
|
+
*
|
|
1602
|
+
* (no flags) Browser link, join existing.
|
|
1603
|
+
* --new-workspace --org-name "X" Browser link, create new free workspace.
|
|
1604
|
+
* --email <addr> OTP flow, join existing.
|
|
1605
|
+
* --email + --new-workspace ... OTP flow, create new free workspace.
|
|
1606
|
+
* --org <slug-or-id> Skip the org picker (existing workspaces).
|
|
1607
|
+
*
|
|
1608
|
+
* Both transports save the resulting `dch_agent_*` session to
|
|
1609
|
+
* `~/.dench/config.json` via `saveSession()` — identical shape to the
|
|
1610
|
+
* previous login flow, so `dench logout` / `dench use` keep working.
|
|
1611
|
+
*/
|
|
1612
|
+
async function signin() {
|
|
1543
1613
|
const scope = resolveSessionScope({ args: filteredArgs });
|
|
1544
1614
|
const explicitKey = explicitSessionKeyInput({ args: filteredArgs });
|
|
1545
1615
|
const host = resolveHostFromArgs(filteredArgs, undefined);
|
|
1546
1616
|
const convexUrl = await discoverConvexUrl(host);
|
|
1547
1617
|
const client = new ConvexHttpClient(convexUrl);
|
|
1548
|
-
|
|
1618
|
+
|
|
1549
1619
|
const sessionToken = `dch_agent_${randomUrlSafe(32)}`;
|
|
1550
|
-
const codeHash = await sha256Hex(code);
|
|
1551
1620
|
const sessionTokenHash = await sha256Hex(sessionToken);
|
|
1621
|
+
|
|
1552
1622
|
const agentKind = normalizeAgentKind(option("--kind"));
|
|
1553
1623
|
const agentName = resolveAgentName(agentKind);
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1624
|
+
const email = option("--email")?.trim().toLowerCase() || null;
|
|
1625
|
+
const newWorkspace = hasFlag("--new-workspace");
|
|
1626
|
+
const orgNameFlag = option("--org-name")?.trim() || null;
|
|
1627
|
+
const orgFlag = option("--org")?.trim() || null;
|
|
1628
|
+
|
|
1629
|
+
if (newWorkspace && !orgNameFlag) {
|
|
1630
|
+
throw new CliError(
|
|
1631
|
+
"--new-workspace requires --org-name '<workspace name>'.",
|
|
1632
|
+
{ code: "missing_org_name" },
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
if (newWorkspace && orgFlag) {
|
|
1636
|
+
throw new CliError(
|
|
1637
|
+
"Pass either --new-workspace --org-name '<name>' OR --org <slug>, not both.",
|
|
1638
|
+
{ code: "conflicting_intent" },
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const intent: SignInIntent = newWorkspace
|
|
1643
|
+
? "create_workspace"
|
|
1644
|
+
: "join_existing";
|
|
1645
|
+
|
|
1646
|
+
if (email) {
|
|
1647
|
+
await signinViaOtp({
|
|
1648
|
+
host,
|
|
1649
|
+
scope,
|
|
1650
|
+
explicitKey,
|
|
1651
|
+
sessionToken,
|
|
1558
1652
|
sessionTokenHash,
|
|
1559
1653
|
agentName,
|
|
1560
1654
|
agentKind,
|
|
1655
|
+
email,
|
|
1656
|
+
intent,
|
|
1657
|
+
proposedOrganizationName: orgNameFlag,
|
|
1658
|
+
orgSelector: orgFlag,
|
|
1659
|
+
});
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
await signinViaBrowser({
|
|
1664
|
+
client,
|
|
1665
|
+
host,
|
|
1666
|
+
convexUrl,
|
|
1667
|
+
scope,
|
|
1668
|
+
explicitKey,
|
|
1669
|
+
sessionToken,
|
|
1670
|
+
sessionTokenHash,
|
|
1671
|
+
agentName,
|
|
1672
|
+
agentKind,
|
|
1673
|
+
intent,
|
|
1674
|
+
proposedOrganizationName: orgNameFlag,
|
|
1675
|
+
orgSelector: orgFlag,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Browser-link variant of `dench signin`. Creates a sign-in request
|
|
1681
|
+
* row in Convex, opens the approval URL in the user's browser, then
|
|
1682
|
+
* polls until the human approves the request (and picks/creates the
|
|
1683
|
+
* target workspace on the approval page).
|
|
1684
|
+
*/
|
|
1685
|
+
async function signinViaBrowser(input: {
|
|
1686
|
+
client: ConvexHttpClient;
|
|
1687
|
+
host: string;
|
|
1688
|
+
convexUrl: string;
|
|
1689
|
+
scope: SessionScope;
|
|
1690
|
+
explicitKey: ReturnType<typeof explicitSessionKeyInput>;
|
|
1691
|
+
sessionToken: string;
|
|
1692
|
+
sessionTokenHash: string;
|
|
1693
|
+
agentName: string;
|
|
1694
|
+
agentKind: string;
|
|
1695
|
+
intent: SignInIntent;
|
|
1696
|
+
proposedOrganizationName: string | null;
|
|
1697
|
+
orgSelector: string | null;
|
|
1698
|
+
}) {
|
|
1699
|
+
const code = randomUrlSafe(18);
|
|
1700
|
+
const codeHash = await sha256Hex(code);
|
|
1701
|
+
const request = await input.client.mutation(
|
|
1702
|
+
api.functions.agentWorkspace.createSignInRequest,
|
|
1703
|
+
{
|
|
1704
|
+
codeHash,
|
|
1705
|
+
sessionTokenHash: input.sessionTokenHash,
|
|
1706
|
+
agentName: input.agentName,
|
|
1707
|
+
agentKind: input.agentKind,
|
|
1708
|
+
intent: input.intent,
|
|
1709
|
+
proposedOrganizationName:
|
|
1710
|
+
input.proposedOrganizationName ?? undefined,
|
|
1711
|
+
requestedOrganizationSelector: input.orgSelector ?? undefined,
|
|
1561
1712
|
userAgent: "@dench.com/cli",
|
|
1562
1713
|
},
|
|
1563
1714
|
);
|
|
1564
|
-
const approvalUrl = `${host}/agent-
|
|
1715
|
+
const approvalUrl = `${input.host}/agent-signin/${encodeURIComponent(code)}`;
|
|
1565
1716
|
|
|
1566
1717
|
const openResult = await openUrl(approvalUrl, {
|
|
1567
1718
|
noOpen: hasFlag("--no-open"),
|
|
@@ -1569,9 +1720,19 @@ async function login() {
|
|
|
1569
1720
|
env: process.env,
|
|
1570
1721
|
});
|
|
1571
1722
|
logHuman(formatApprovalOpenMessage(approvalUrl, openResult));
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1723
|
+
if (input.intent === "create_workspace") {
|
|
1724
|
+
logHuman(
|
|
1725
|
+
`On the approval page, confirm the workspace name and connect ${input.agentName}.`,
|
|
1726
|
+
);
|
|
1727
|
+
} else if (input.orgSelector) {
|
|
1728
|
+
logHuman(
|
|
1729
|
+
`On the approval page, confirm connecting ${input.agentName} to ${input.orgSelector}.`,
|
|
1730
|
+
);
|
|
1731
|
+
} else {
|
|
1732
|
+
logHuman(
|
|
1733
|
+
`On the approval page, pick which workspace to connect ${input.agentName} to (or create a new one).`,
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1575
1736
|
logHuman(
|
|
1576
1737
|
`This request expires at ${new Date(request.expiresAt).toLocaleString()}.`,
|
|
1577
1738
|
);
|
|
@@ -1579,33 +1740,33 @@ async function login() {
|
|
|
1579
1740
|
const timeoutMs = Number(option("--timeout-ms") ?? 15 * 60 * 1000);
|
|
1580
1741
|
const deadline = Date.now() + timeoutMs;
|
|
1581
1742
|
while (Date.now() < deadline) {
|
|
1582
|
-
const poll = await client.mutation(
|
|
1583
|
-
api.functions.agentWorkspace.
|
|
1584
|
-
{ codeHash, sessionTokenHash },
|
|
1743
|
+
const poll = await input.client.mutation(
|
|
1744
|
+
api.functions.agentWorkspace.pollSignInRequest,
|
|
1745
|
+
{ codeHash, sessionTokenHash: input.sessionTokenHash },
|
|
1585
1746
|
);
|
|
1586
1747
|
|
|
1587
1748
|
if (poll.status === "approved") {
|
|
1588
1749
|
const storedSession: StoredSession = {
|
|
1589
|
-
host,
|
|
1590
|
-
convexUrl,
|
|
1591
|
-
sessionToken,
|
|
1750
|
+
host: input.host,
|
|
1751
|
+
convexUrl: input.convexUrl,
|
|
1752
|
+
sessionToken: input.sessionToken,
|
|
1592
1753
|
organization: poll.organization,
|
|
1593
1754
|
agent: poll.agent,
|
|
1594
1755
|
sessionExpiresAt: poll.sessionExpiresAt,
|
|
1595
1756
|
savedAt: Date.now(),
|
|
1596
1757
|
};
|
|
1597
|
-
await saveSession(storedSession, scope);
|
|
1598
|
-
const message =
|
|
1758
|
+
await saveSession(storedSession, input.scope);
|
|
1759
|
+
const message = signinNextCommandsMessage(
|
|
1599
1760
|
storedSession,
|
|
1600
|
-
explicitKey?.source === "DENCH_SESSION_KEY"
|
|
1601
|
-
? explicitKey.value
|
|
1761
|
+
input.explicitKey?.source === "DENCH_SESSION_KEY"
|
|
1762
|
+
? input.explicitKey.value
|
|
1602
1763
|
: undefined,
|
|
1603
1764
|
);
|
|
1604
1765
|
print(
|
|
1605
1766
|
json
|
|
1606
1767
|
? {
|
|
1607
1768
|
ok: true,
|
|
1608
|
-
host,
|
|
1769
|
+
host: input.host,
|
|
1609
1770
|
organization: poll.organization,
|
|
1610
1771
|
agent: poll.agent,
|
|
1611
1772
|
sessionExpiresAt: poll.sessionExpiresAt,
|
|
@@ -1616,13 +1777,264 @@ async function login() {
|
|
|
1616
1777
|
return;
|
|
1617
1778
|
}
|
|
1618
1779
|
if (poll.status === "rejected" || poll.status === "expired") {
|
|
1619
|
-
throw new Error(`
|
|
1780
|
+
throw new Error(`Sign-in ${poll.status}`);
|
|
1620
1781
|
}
|
|
1621
1782
|
|
|
1622
1783
|
await sleep(2000);
|
|
1623
1784
|
}
|
|
1624
1785
|
|
|
1625
|
-
throw new Error(
|
|
1786
|
+
throw new Error(
|
|
1787
|
+
`Sign-in timed out. Open ${approvalUrl} and run \`dench signin\` again.`,
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
type OtpVerifyResult =
|
|
1792
|
+
| {
|
|
1793
|
+
kind: "completed";
|
|
1794
|
+
organization?: { id: string; name: string; slug: string };
|
|
1795
|
+
sessionExpiresAt?: number;
|
|
1796
|
+
}
|
|
1797
|
+
| {
|
|
1798
|
+
kind: "requiresOrgChoice";
|
|
1799
|
+
authToken: string;
|
|
1800
|
+
organizations: Array<{ id: string; name: string; slug: string }>;
|
|
1801
|
+
expiresAt: number;
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* OTP variant of `dench signin`. Sends the OTP via the Next.js
|
|
1806
|
+
* agent-signin routes, prompts the human for the 6-digit code, and
|
|
1807
|
+
* mints a session against the org they pick (or create) in one or
|
|
1808
|
+
* two HTTP round-trips.
|
|
1809
|
+
*/
|
|
1810
|
+
async function signinViaOtp(input: {
|
|
1811
|
+
host: string;
|
|
1812
|
+
scope: SessionScope;
|
|
1813
|
+
explicitKey: ReturnType<typeof explicitSessionKeyInput>;
|
|
1814
|
+
sessionToken: string;
|
|
1815
|
+
sessionTokenHash: string;
|
|
1816
|
+
agentName: string;
|
|
1817
|
+
agentKind: string;
|
|
1818
|
+
email: string;
|
|
1819
|
+
intent: SignInIntent;
|
|
1820
|
+
proposedOrganizationName: string | null;
|
|
1821
|
+
orgSelector: string | null;
|
|
1822
|
+
}) {
|
|
1823
|
+
const base = input.host.replace(/\/+$/, "");
|
|
1824
|
+
|
|
1825
|
+
// Step 1 — send the OTP.
|
|
1826
|
+
const startResponse = await fetch(`${base}/api/agent-signin/otp-start`, {
|
|
1827
|
+
method: "POST",
|
|
1828
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1829
|
+
body: JSON.stringify({ email: input.email }),
|
|
1830
|
+
});
|
|
1831
|
+
if (!startResponse.ok) {
|
|
1832
|
+
const body = await startResponse.text();
|
|
1833
|
+
throw new CliError(
|
|
1834
|
+
`OTP send failed (${startResponse.status}): ${body || "unknown error"}`,
|
|
1835
|
+
{ code: "otp_send_failed", status: startResponse.status },
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
logHuman(`We sent a 6-digit sign-in code to ${input.email}.`);
|
|
1839
|
+
|
|
1840
|
+
// Step 2 — verify the OTP. Either the user is unambiguously
|
|
1841
|
+
// resolvable (single admin org or create_workspace intent) and the
|
|
1842
|
+
// server hands us a session, OR we get an auth token + list and
|
|
1843
|
+
// need a third call.
|
|
1844
|
+
const code = option("--otp-code")?.trim() || (await promptLine("Paste OTP: "));
|
|
1845
|
+
if (!/^\d{6}$/.test(code)) {
|
|
1846
|
+
throw new CliError("OTP code must be exactly 6 digits.", {
|
|
1847
|
+
code: "invalid_otp_code",
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
const verifyResponse = await fetch(`${base}/api/agent-signin/otp-verify`, {
|
|
1851
|
+
method: "POST",
|
|
1852
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1853
|
+
body: JSON.stringify({
|
|
1854
|
+
email: input.email,
|
|
1855
|
+
code,
|
|
1856
|
+
agentName: input.agentName,
|
|
1857
|
+
agentKind: input.agentKind,
|
|
1858
|
+
sessionTokenHash: input.sessionTokenHash,
|
|
1859
|
+
intent: input.intent,
|
|
1860
|
+
proposedOrganizationName:
|
|
1861
|
+
input.proposedOrganizationName ?? undefined,
|
|
1862
|
+
organizationSelector: input.orgSelector ?? undefined,
|
|
1863
|
+
userAgent: "@dench.com/cli",
|
|
1864
|
+
}),
|
|
1865
|
+
});
|
|
1866
|
+
if (!verifyResponse.ok) {
|
|
1867
|
+
const body = await verifyResponse.text();
|
|
1868
|
+
throw new CliError(
|
|
1869
|
+
`OTP verify failed (${verifyResponse.status}): ${body || "unknown error"}`,
|
|
1870
|
+
{ code: "otp_verify_failed", status: verifyResponse.status },
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
const verifyPayload = (await verifyResponse.json()) as {
|
|
1874
|
+
ok: boolean;
|
|
1875
|
+
result: OtpVerifyResult;
|
|
1876
|
+
};
|
|
1877
|
+
const result = verifyPayload.result;
|
|
1878
|
+
|
|
1879
|
+
// Branch A: server minted the session in one shot.
|
|
1880
|
+
if (result.kind === "completed") {
|
|
1881
|
+
assertOtpCompletedOrgMatchesSelector(result.organization, input.orgSelector);
|
|
1882
|
+
await finalizeOtpSession(input, result.organization, result.sessionExpiresAt);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// Branch B: human has multiple admin orgs — pick one (or create new).
|
|
1887
|
+
const finalizeChoice = await resolveOrgChoiceForOtp({
|
|
1888
|
+
organizations: result.organizations,
|
|
1889
|
+
orgSelector: input.orgSelector,
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
const finalizeResponse = await fetch(
|
|
1893
|
+
`${base}/api/agent-signin/otp-finalize`,
|
|
1894
|
+
{
|
|
1895
|
+
method: "POST",
|
|
1896
|
+
headers: {
|
|
1897
|
+
"content-type": "application/json",
|
|
1898
|
+
accept: "application/json",
|
|
1899
|
+
},
|
|
1900
|
+
body: JSON.stringify({
|
|
1901
|
+
authToken: result.authToken,
|
|
1902
|
+
organizationId: finalizeChoice.organizationId,
|
|
1903
|
+
newOrganizationName: finalizeChoice.newOrganizationName,
|
|
1904
|
+
}),
|
|
1905
|
+
},
|
|
1906
|
+
);
|
|
1907
|
+
if (!finalizeResponse.ok) {
|
|
1908
|
+
const body = await finalizeResponse.text();
|
|
1909
|
+
throw new CliError(
|
|
1910
|
+
`OTP finalize failed (${finalizeResponse.status}): ${body || "unknown error"}`,
|
|
1911
|
+
{ code: "otp_finalize_failed", status: finalizeResponse.status },
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
const finalizePayload = (await finalizeResponse.json()) as {
|
|
1915
|
+
ok: boolean;
|
|
1916
|
+
result: {
|
|
1917
|
+
organization?: { id: string; name: string; slug: string };
|
|
1918
|
+
sessionExpiresAt?: number;
|
|
1919
|
+
};
|
|
1920
|
+
};
|
|
1921
|
+
await finalizeOtpSession(
|
|
1922
|
+
input,
|
|
1923
|
+
finalizePayload.result.organization,
|
|
1924
|
+
finalizePayload.result.sessionExpiresAt,
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function assertOtpCompletedOrgMatchesSelector(
|
|
1929
|
+
organization: { id: string; name: string; slug: string } | undefined,
|
|
1930
|
+
orgSelector: string | null,
|
|
1931
|
+
) {
|
|
1932
|
+
if (!orgSelector) return;
|
|
1933
|
+
const normalized = orgSelector.trim().toLowerCase();
|
|
1934
|
+
if (!organization) {
|
|
1935
|
+
throw new CliError(
|
|
1936
|
+
`The OTP flow completed without an organization, but --org ${orgSelector} was requested.`,
|
|
1937
|
+
{ code: "org_not_found" },
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
const matches =
|
|
1941
|
+
organization.id.toLowerCase() === normalized ||
|
|
1942
|
+
organization.slug.toLowerCase() === normalized ||
|
|
1943
|
+
organization.name.trim().toLowerCase() === normalized;
|
|
1944
|
+
if (!matches) {
|
|
1945
|
+
throw new CliError(
|
|
1946
|
+
`The OTP flow resolved ${organization.name} (${organization.slug}), but --org ${orgSelector} was requested.`,
|
|
1947
|
+
{ code: "org_mismatch" },
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
async function finalizeOtpSession(
|
|
1953
|
+
input: {
|
|
1954
|
+
host: string;
|
|
1955
|
+
scope: SessionScope;
|
|
1956
|
+
explicitKey: ReturnType<typeof explicitSessionKeyInput>;
|
|
1957
|
+
sessionToken: string;
|
|
1958
|
+
},
|
|
1959
|
+
organization: { id: string; name: string; slug: string } | undefined,
|
|
1960
|
+
sessionExpiresAt: number | undefined,
|
|
1961
|
+
) {
|
|
1962
|
+
const convexUrl = await discoverConvexUrl(input.host);
|
|
1963
|
+
const storedSession: StoredSession = {
|
|
1964
|
+
host: input.host,
|
|
1965
|
+
convexUrl,
|
|
1966
|
+
sessionToken: input.sessionToken,
|
|
1967
|
+
organization,
|
|
1968
|
+
sessionExpiresAt,
|
|
1969
|
+
savedAt: Date.now(),
|
|
1970
|
+
};
|
|
1971
|
+
await saveSession(storedSession, input.scope);
|
|
1972
|
+
const message = signinNextCommandsMessage(
|
|
1973
|
+
storedSession,
|
|
1974
|
+
input.explicitKey?.source === "DENCH_SESSION_KEY"
|
|
1975
|
+
? input.explicitKey.value
|
|
1976
|
+
: undefined,
|
|
1977
|
+
);
|
|
1978
|
+
print(
|
|
1979
|
+
json
|
|
1980
|
+
? {
|
|
1981
|
+
ok: true,
|
|
1982
|
+
host: input.host,
|
|
1983
|
+
organization,
|
|
1984
|
+
sessionExpiresAt,
|
|
1985
|
+
message,
|
|
1986
|
+
}
|
|
1987
|
+
: message,
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
async function resolveOrgChoiceForOtp(input: {
|
|
1992
|
+
organizations: Array<{ id: string; name: string; slug: string }>;
|
|
1993
|
+
orgSelector: string | null;
|
|
1994
|
+
}): Promise<{ organizationId?: string; newOrganizationName?: string }> {
|
|
1995
|
+
if (input.orgSelector) {
|
|
1996
|
+
const match = input.organizations.find(
|
|
1997
|
+
(org) =>
|
|
1998
|
+
org.id === input.orgSelector ||
|
|
1999
|
+
org.slug === input.orgSelector ||
|
|
2000
|
+
org.name === input.orgSelector,
|
|
2001
|
+
);
|
|
2002
|
+
if (!match) {
|
|
2003
|
+
throw new CliError(
|
|
2004
|
+
`--org "${input.orgSelector}" did not match any of your admin workspaces.`,
|
|
2005
|
+
{ code: "org_not_found" },
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
return { organizationId: match.id };
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Interactive picker.
|
|
2012
|
+
logHuman("");
|
|
2013
|
+
logHuman("Which workspace should this agent connect to?");
|
|
2014
|
+
for (let i = 0; i < input.organizations.length; i++) {
|
|
2015
|
+
const org = input.organizations[i];
|
|
2016
|
+
logHuman(` ${i + 1}) ${org.name} (${org.slug})`);
|
|
2017
|
+
}
|
|
2018
|
+
logHuman(` ${input.organizations.length + 1}) Create a new free workspace`);
|
|
2019
|
+
logHuman("");
|
|
2020
|
+
|
|
2021
|
+
const answer = await promptLine("Pick a number: ");
|
|
2022
|
+
const choice = Number(answer);
|
|
2023
|
+
if (!Number.isFinite(choice) || choice < 1 || choice > input.organizations.length + 1) {
|
|
2024
|
+
throw new CliError(`Invalid choice: ${answer}`, {
|
|
2025
|
+
code: "invalid_org_choice",
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
if (choice <= input.organizations.length) {
|
|
2029
|
+
return { organizationId: input.organizations[choice - 1].id };
|
|
2030
|
+
}
|
|
2031
|
+
const newName = await promptLine("New workspace name: ");
|
|
2032
|
+
if (!newName.trim()) {
|
|
2033
|
+
throw new CliError("Workspace name cannot be empty.", {
|
|
2034
|
+
code: "missing_org_name",
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
return { newOrganizationName: newName.trim() };
|
|
1626
2038
|
}
|
|
1627
2039
|
|
|
1628
2040
|
function normalizeApiBase(input: string) {
|
|
@@ -1745,6 +2157,116 @@ async function billingTopup(runtime: Runtime) {
|
|
|
1745
2157
|
print(json ? output : output.message);
|
|
1746
2158
|
}
|
|
1747
2159
|
|
|
2160
|
+
/**
|
|
2161
|
+
* `dench upgrade` — turns a free CRM-only workspace into a paid Pro or
|
|
2162
|
+
* Max plan by handing the human a Stripe Checkout link. Mirrors the
|
|
2163
|
+
* `billing topup` flow (POST with the agent session as a Bearer
|
|
2164
|
+
* token, surface the returned URL) but hits the existing
|
|
2165
|
+
* `/api/stripe/create-checkout` endpoint that the marketing pricing
|
|
2166
|
+
* page already uses.
|
|
2167
|
+
*
|
|
2168
|
+
* Two flag-driven paths:
|
|
2169
|
+
*
|
|
2170
|
+
* --tier pro | max Which paid tier to target. Defaults to
|
|
2171
|
+
* "pro" (the most common upgrade from
|
|
2172
|
+
* free).
|
|
2173
|
+
* --cycle monthly | yearly Billing cycle. Defaults to "monthly".
|
|
2174
|
+
*
|
|
2175
|
+
* The endpoint can also return `{ updated: true }` for in-place
|
|
2176
|
+
* Pro \u2192 Max upgrades on an already-subscribed org (no Checkout URL
|
|
2177
|
+
* needed in that case). We surface either outcome cleanly.
|
|
2178
|
+
*/
|
|
2179
|
+
async function denchUpgrade(runtime: Runtime) {
|
|
2180
|
+
const sessionRuntime = requireSessionRuntime(runtime, "dench upgrade");
|
|
2181
|
+
const tierFlag = option("--tier")?.trim().toLowerCase() || "pro";
|
|
2182
|
+
const cycleFlag =
|
|
2183
|
+
option("--cycle")?.trim().toLowerCase() ||
|
|
2184
|
+
option("--billing-cycle")?.trim().toLowerCase() ||
|
|
2185
|
+
"monthly";
|
|
2186
|
+
if (tierFlag !== "pro" && tierFlag !== "max") {
|
|
2187
|
+
throw new CliError("--tier must be 'pro' or 'max'.", {
|
|
2188
|
+
code: "invalid_tier",
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
if (cycleFlag !== "monthly" && cycleFlag !== "yearly") {
|
|
2192
|
+
throw new CliError("--cycle must be 'monthly' or 'yearly'.", {
|
|
2193
|
+
code: "invalid_cycle",
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
// Internal tier strings still use the legacy "desktop" / "cloud"
|
|
2197
|
+
// names from before the Daytona rewrite. Map the CLI's
|
|
2198
|
+
// customer-facing flag onto them so the existing checkout endpoint
|
|
2199
|
+
// doesn't need to learn a new vocabulary.
|
|
2200
|
+
const internalTier = tierFlag === "pro" ? "desktop" : "cloud";
|
|
2201
|
+
|
|
2202
|
+
const response = await fetch(
|
|
2203
|
+
`${sessionRuntime.host.replace(/\/+$/, "")}/api/stripe/create-checkout`,
|
|
2204
|
+
{
|
|
2205
|
+
method: "POST",
|
|
2206
|
+
headers: {
|
|
2207
|
+
accept: "application/json",
|
|
2208
|
+
"content-type": "application/json",
|
|
2209
|
+
authorization: `Bearer ${sessionRuntime.sessionToken}`,
|
|
2210
|
+
},
|
|
2211
|
+
body: JSON.stringify({
|
|
2212
|
+
tier: internalTier,
|
|
2213
|
+
billingCycle: cycleFlag,
|
|
2214
|
+
requestId: crypto.randomUUID(),
|
|
2215
|
+
}),
|
|
2216
|
+
},
|
|
2217
|
+
);
|
|
2218
|
+
const payload = await parseResponseBody(response);
|
|
2219
|
+
if (!response.ok) {
|
|
2220
|
+
const errorRecord = asRecord(payload);
|
|
2221
|
+
throw new CliError(
|
|
2222
|
+
stringField(errorRecord, "error", "message") ??
|
|
2223
|
+
`Could not create upgrade checkout (${response.status})`,
|
|
2224
|
+
{
|
|
2225
|
+
code: stringField(errorRecord, "code") ?? "upgrade_failed",
|
|
2226
|
+
status: response.status,
|
|
2227
|
+
},
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
const record = asRecord(payload);
|
|
2231
|
+
const url = stringField(record, "url");
|
|
2232
|
+
const updatedInPlace = record?.updated === true;
|
|
2233
|
+
|
|
2234
|
+
if (updatedInPlace) {
|
|
2235
|
+
const message = `Upgraded to Dench ${tierFlag === "pro" ? "Pro" : "Max"} in place — no Stripe Checkout needed.`;
|
|
2236
|
+
print(
|
|
2237
|
+
json
|
|
2238
|
+
? { ok: true, updated: true, tier: tierFlag, message }
|
|
2239
|
+
: message,
|
|
2240
|
+
);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
const openResult = url
|
|
2245
|
+
? await openUrl(url, {
|
|
2246
|
+
noOpen: hasFlag("--no-open"),
|
|
2247
|
+
json,
|
|
2248
|
+
env: process.env,
|
|
2249
|
+
})
|
|
2250
|
+
: ({ status: "skipped", reason: "missing-url" } as const);
|
|
2251
|
+
const openMessage =
|
|
2252
|
+
url && openResult.status === "opened"
|
|
2253
|
+
? `Attempted to open Stripe Checkout in your browser. If it did not open, use this link: ${url}`
|
|
2254
|
+
: url && openResult.status === "failed"
|
|
2255
|
+
? `Could not open browser automatically. Open this Stripe Checkout link: ${url}`
|
|
2256
|
+
: url
|
|
2257
|
+
? `Open this Stripe Checkout link: ${url}`
|
|
2258
|
+
: "Stripe Checkout link created.";
|
|
2259
|
+
const output = {
|
|
2260
|
+
ok: true,
|
|
2261
|
+
tier: tierFlag,
|
|
2262
|
+
billingCycle: cycleFlag,
|
|
2263
|
+
url,
|
|
2264
|
+
openResult,
|
|
2265
|
+
message: openMessage,
|
|
2266
|
+
};
|
|
2267
|
+
print(json ? output : output.message);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
1748
2270
|
function hasDevEnv() {
|
|
1749
2271
|
return Boolean(
|
|
1750
2272
|
process.env.NEXT_PUBLIC_CONVEX_URL &&
|
|
@@ -1794,11 +2316,11 @@ function resolveApiHost(fallbackHost: string): string {
|
|
|
1794
2316
|
}
|
|
1795
2317
|
|
|
1796
2318
|
function loginRequiredError(command = "this command") {
|
|
1797
|
-
return new CliError(`${command} requires dench
|
|
2319
|
+
return new CliError(`${command} requires dench signin`, {
|
|
1798
2320
|
code: "login_required",
|
|
1799
2321
|
nextActions: [
|
|
1800
2322
|
"Set DENCH_API_KEY (and CONVEX_URL) in your env if running inside a sandbox or CI.",
|
|
1801
|
-
'Or run dench
|
|
2323
|
+
'Or run dench signin --kind <kind> --name "AI Agent - Project" to mint an agent session.',
|
|
1802
2324
|
"If already approved, run dench sessions --json, then dench use <session-key-or-workspace-slug>.",
|
|
1803
2325
|
],
|
|
1804
2326
|
});
|
|
@@ -1815,7 +2337,7 @@ function missingDevEnvError() {
|
|
|
1815
2337
|
missingEnv: required.filter((name) => !process.env[name]?.trim()),
|
|
1816
2338
|
nextActions: [
|
|
1817
2339
|
"Set NEXT_PUBLIC_CONVEX_URL, DENCH_WORKSPACE, and DENCH_DEV_AGENT_KEY.",
|
|
1818
|
-
"Or remove --dev and run dench
|
|
2340
|
+
"Or remove --dev and run dench signin to use a real agent session.",
|
|
1819
2341
|
],
|
|
1820
2342
|
});
|
|
1821
2343
|
}
|
|
@@ -1871,7 +2393,7 @@ async function getRuntime() {
|
|
|
1871
2393
|
// src/lib/secrets/sandbox-tokens.ts) is enough to drive every
|
|
1872
2394
|
// command that goes through `requireCrmAccess` server-side. We only
|
|
1873
2395
|
// fall into this branch when no stored session is present, so
|
|
1874
|
-
// explicit `dench
|
|
2396
|
+
// explicit `dench signin` setups keep their existing behavior.
|
|
1875
2397
|
const apiKey = process.env.DENCH_API_KEY?.trim();
|
|
1876
2398
|
const convexUrl = resolveApiKeyConvexUrl();
|
|
1877
2399
|
if (apiKey && convexUrl) {
|
|
@@ -1978,7 +2500,7 @@ function agentSessionRequiredError(command: string) {
|
|
|
1978
2500
|
{
|
|
1979
2501
|
code: "agent_session_required",
|
|
1980
2502
|
nextActions: [
|
|
1981
|
-
'Run dench
|
|
2503
|
+
'Run dench signin --kind <kind> --name "AI Agent - Project" to mint an agent session for this command.',
|
|
1982
2504
|
"Or run the equivalent action via the workspace UI / dench crm if it is org-level (e.g. CRM data, files).",
|
|
1983
2505
|
],
|
|
1984
2506
|
},
|
|
@@ -2079,7 +2601,7 @@ function workspaceUrls(host: string, workspaceSlug: string | undefined) {
|
|
|
2079
2601
|
|
|
2080
2602
|
function nextContextCommands() {
|
|
2081
2603
|
return [
|
|
2082
|
-
"dench
|
|
2604
|
+
"dench context --json",
|
|
2083
2605
|
"dench tasks --json",
|
|
2084
2606
|
'dench log "Brief progress update"',
|
|
2085
2607
|
'dench tool search "what you need" --toolkit <app>',
|
|
@@ -2135,36 +2657,6 @@ async function buildContext(runtime: Runtime) {
|
|
|
2135
2657
|
};
|
|
2136
2658
|
}
|
|
2137
2659
|
|
|
2138
|
-
async function runOnboarding() {
|
|
2139
|
-
let runtime: Runtime;
|
|
2140
|
-
try {
|
|
2141
|
-
runtime = await getRuntime();
|
|
2142
|
-
} catch (error) {
|
|
2143
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2144
|
-
const isLoginRequired =
|
|
2145
|
-
error instanceof CliError && error.payload.code === "login_required";
|
|
2146
|
-
if (!isLoginRequired && !message.includes("Not logged in")) {
|
|
2147
|
-
throw error;
|
|
2148
|
-
}
|
|
2149
|
-
logHuman("No Dench session found. Starting login first.");
|
|
2150
|
-
await login();
|
|
2151
|
-
runtime = await getRuntime();
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
const context = await buildContext(runtime);
|
|
2155
|
-
print(
|
|
2156
|
-
json
|
|
2157
|
-
? { ok: true, context }
|
|
2158
|
-
: [
|
|
2159
|
-
"Dench onboarding complete.",
|
|
2160
|
-
"",
|
|
2161
|
-
formatContext(context),
|
|
2162
|
-
"",
|
|
2163
|
-
"Setup is complete when the workspace and agent above are correct.",
|
|
2164
|
-
].join("\n"),
|
|
2165
|
-
);
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
2660
|
function formatCount(value: unknown) {
|
|
2169
2661
|
return typeof value === "number" ? String(value) : "0";
|
|
2170
2662
|
}
|
|
@@ -2441,7 +2933,7 @@ async function defaultDevAgentId(
|
|
|
2441
2933
|
const agent = data.agents[0];
|
|
2442
2934
|
if (!agent) {
|
|
2443
2935
|
throw new Error(
|
|
2444
|
-
'No agent registered yet. Run: dench
|
|
2936
|
+
'No agent registered yet. Run: dench signin --kind <kind> --name "AI Agent - Project"',
|
|
2445
2937
|
);
|
|
2446
2938
|
}
|
|
2447
2939
|
return agent._id;
|
|
@@ -2546,7 +3038,7 @@ function requireFilesApiKey(runtime: Runtime, command: string): string {
|
|
|
2546
3038
|
code: "files_api_key_required",
|
|
2547
3039
|
nextActions: [
|
|
2548
3040
|
"Run inside a Dench sandbox where DENCH_API_KEY is baked in.",
|
|
2549
|
-
"Or export DENCH_API_KEY=<key> first (see dench
|
|
3041
|
+
"Or export DENCH_API_KEY=<key> first (see dench signin output).",
|
|
2550
3042
|
],
|
|
2551
3043
|
},
|
|
2552
3044
|
);
|
|
@@ -3182,21 +3674,42 @@ async function main() {
|
|
|
3182
3674
|
return;
|
|
3183
3675
|
}
|
|
3184
3676
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3677
|
+
const isDeprecatedAuthCommand =
|
|
3678
|
+
command === "login" ||
|
|
3679
|
+
command === "onboard" ||
|
|
3680
|
+
command === "setup" ||
|
|
3681
|
+
command === "what-can-i-do" ||
|
|
3682
|
+
command === "register";
|
|
3189
3683
|
|
|
3190
3684
|
if (
|
|
3191
|
-
(command === "
|
|
3192
|
-
command === "setup" ||
|
|
3193
|
-
command === "what-can-i-do") &&
|
|
3685
|
+
(command === "signin" || isDeprecatedAuthCommand) &&
|
|
3194
3686
|
(subcommand === "help" || hasFlag("--help"))
|
|
3195
3687
|
) {
|
|
3196
|
-
|
|
3688
|
+
signinHelp();
|
|
3197
3689
|
return;
|
|
3198
3690
|
}
|
|
3199
3691
|
|
|
3692
|
+
// `dench login`, `dench onboard`, `dench setup`, `dench what-can-i-do`,
|
|
3693
|
+
// and `dench register` were removed in @dench.com/cli v2. Surface a
|
|
3694
|
+
// clean redirect to the new unified surface so old `npx` caches and
|
|
3695
|
+
// pre-existing agent prompts get a one-line nudge instead of an
|
|
3696
|
+
// opaque "Unknown command" error.
|
|
3697
|
+
if (
|
|
3698
|
+
isDeprecatedAuthCommand
|
|
3699
|
+
) {
|
|
3700
|
+
throw new CliError(
|
|
3701
|
+
`\`dench ${command}\` was removed in @dench.com/cli v2. Run \`dench signin\` instead.`,
|
|
3702
|
+
{
|
|
3703
|
+
code: "deprecated_command",
|
|
3704
|
+
nextActions: [
|
|
3705
|
+
'Run `dench signin` to connect this agent to an existing workspace.',
|
|
3706
|
+
'Run `dench signin --new-workspace --org-name "<name>"` to create a free CRM-only workspace.',
|
|
3707
|
+
"Upgrade the CLI globally with `npm install -g @dench.com/cli@latest`.",
|
|
3708
|
+
],
|
|
3709
|
+
},
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3200
3713
|
if (command === "context" && hasFlag("--help")) {
|
|
3201
3714
|
contextHelp();
|
|
3202
3715
|
return;
|
|
@@ -3258,7 +3771,7 @@ async function main() {
|
|
|
3258
3771
|
const { runCrmCommand } = await import("./crm");
|
|
3259
3772
|
const runtime = await getRuntime();
|
|
3260
3773
|
// CRM is org-scoped — accept either an agent session token
|
|
3261
|
-
// (`dch_agent_*` from `dench
|
|
3774
|
+
// (`dch_agent_*` from `dench signin`) or a unified Dench API key
|
|
3262
3775
|
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
3263
3776
|
// `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
|
|
3264
3777
|
// bearer prefix and resolves the org for both.
|
|
@@ -3485,7 +3998,7 @@ async function main() {
|
|
|
3485
3998
|
const { runCronCommand } = await import("./cron");
|
|
3486
3999
|
const runtime = await getRuntime();
|
|
3487
4000
|
// Cron CRUD is org-scoped — accept either an agent session token
|
|
3488
|
-
// (`dch_agent_*` from `dench
|
|
4001
|
+
// (`dch_agent_*` from `dench signin`) or a unified Dench API key
|
|
3489
4002
|
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
3490
4003
|
// `requireCronAccess` (convex/lib/cronAccess.ts) dispatches on the
|
|
3491
4004
|
// bearer prefix and resolves the org for both. The Bearer token
|
|
@@ -3508,7 +4021,7 @@ async function main() {
|
|
|
3508
4021
|
// Dispatch all `dench identity / organisation / user / tools / mem /
|
|
3509
4022
|
// heartbeat / bootstrap / model / daily` traffic through the shared
|
|
3510
4023
|
// agent-config CLI module. Each command requires the same auth shape
|
|
3511
|
-
// as `dench cron` (Bearer DENCH_API_KEY or `dench
|
|
4024
|
+
// as `dench cron` (Bearer DENCH_API_KEY or `dench signin` session
|
|
3512
4025
|
// token) so server-side `requireCronAccess` accepts both.
|
|
3513
4026
|
const AGENT_CONFIG_COMMANDS = new Set([
|
|
3514
4027
|
"identity",
|
|
@@ -3582,17 +4095,8 @@ async function main() {
|
|
|
3582
4095
|
}
|
|
3583
4096
|
}
|
|
3584
4097
|
|
|
3585
|
-
if (command === "
|
|
3586
|
-
await
|
|
3587
|
-
return;
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
if (
|
|
3591
|
-
command === "onboard" ||
|
|
3592
|
-
command === "setup" ||
|
|
3593
|
-
command === "what-can-i-do"
|
|
3594
|
-
) {
|
|
3595
|
-
await runOnboarding();
|
|
4098
|
+
if (command === "signin") {
|
|
4099
|
+
await signin();
|
|
3596
4100
|
return;
|
|
3597
4101
|
}
|
|
3598
4102
|
|
|
@@ -3631,6 +4135,11 @@ async function main() {
|
|
|
3631
4135
|
return;
|
|
3632
4136
|
}
|
|
3633
4137
|
|
|
4138
|
+
if (command === "upgrade") {
|
|
4139
|
+
await denchUpgrade(runtime);
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
3634
4143
|
if (command === "context") {
|
|
3635
4144
|
const context = await buildContext(runtime);
|
|
3636
4145
|
print(json ? context : formatContext(context));
|
|
@@ -3828,24 +4337,6 @@ async function main() {
|
|
|
3828
4337
|
return;
|
|
3829
4338
|
}
|
|
3830
4339
|
|
|
3831
|
-
if (command === "register") {
|
|
3832
|
-
if (runtime.mode !== "dev") {
|
|
3833
|
-
throw new Error(
|
|
3834
|
-
"register is only needed in dev mode. Real sessions use dench login.",
|
|
3835
|
-
);
|
|
3836
|
-
}
|
|
3837
|
-
const agentId = await runtime.client.mutation(
|
|
3838
|
-
api.functions.agentWorkspace.devRegisterAgent,
|
|
3839
|
-
{
|
|
3840
|
-
...runtime.workspaceArgs,
|
|
3841
|
-
name: option("--name") ?? "AI Agent",
|
|
3842
|
-
kind: normalizeAgentKind(option("--kind")),
|
|
3843
|
-
},
|
|
3844
|
-
);
|
|
3845
|
-
print({ ok: true, agentId });
|
|
3846
|
-
return;
|
|
3847
|
-
}
|
|
3848
|
-
|
|
3849
4340
|
if (command === "project" && subcommand === "create") {
|
|
3850
4341
|
if (runtime.mode !== "dev") {
|
|
3851
4342
|
throw new Error(
|
package/host.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PRODUCTION_HOST = "https://dench.
|
|
1
|
+
export const PRODUCTION_HOST = "https://dench.com";
|
|
2
2
|
export const STAGING_HOST = "https://workspace-staging.dench.com";
|
|
3
3
|
export const LOCAL_HOST = "http://localhost:3000";
|
|
4
4
|
export const DEFAULT_HOST = PRODUCTION_HOST;
|
|
@@ -63,7 +63,7 @@ export function resolveBackendAlias(input: string): string {
|
|
|
63
63
|
const value = input.trim();
|
|
64
64
|
if (!value) {
|
|
65
65
|
throw new Error(
|
|
66
|
-
"Backend value is empty. Pass local, staging, prod, or a URL like https://dench.
|
|
66
|
+
"Backend value is empty. Pass local, staging, prod, or a URL like https://dench.com.",
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
const normalized = value.toLowerCase();
|
|
@@ -80,7 +80,7 @@ export function resolveBackendAlias(input: string): string {
|
|
|
80
80
|
return normalizeHost(value);
|
|
81
81
|
} catch (_error) {
|
|
82
82
|
throw new Error(
|
|
83
|
-
`Unrecognized backend "${value}". Use local, staging, prod, or a URL like https://dench.
|
|
83
|
+
`Unrecognized backend "${value}". Use local, staging, prod, or a URL like https://dench.com.`,
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -161,6 +161,7 @@ export type ApolloPersonEnvelope = {
|
|
|
161
161
|
linkedin_url?: string;
|
|
162
162
|
title?: string;
|
|
163
163
|
headline?: string;
|
|
164
|
+
location?: string;
|
|
164
165
|
contact: {
|
|
165
166
|
phone_numbers: Array<{
|
|
166
167
|
sanitized_number: string;
|
|
@@ -178,7 +179,12 @@ type EnrichedPersonUpsertLike = {
|
|
|
178
179
|
/** Structured title from the person's current role (distinct from headline). */
|
|
179
180
|
currentTitle?: string;
|
|
180
181
|
linkedinID?: string;
|
|
181
|
-
|
|
182
|
+
location?: string;
|
|
183
|
+
emails?: Array<{
|
|
184
|
+
address?: string;
|
|
185
|
+
normalizedAddress?: string;
|
|
186
|
+
type?: string;
|
|
187
|
+
}>;
|
|
182
188
|
phones?: Array<{ number?: string; normalizedNumber?: string }>;
|
|
183
189
|
};
|
|
184
190
|
|
|
@@ -201,6 +207,7 @@ function shapePersonToApolloEnvelope(
|
|
|
201
207
|
: undefined,
|
|
202
208
|
title: u.currentTitle,
|
|
203
209
|
headline: u.headline,
|
|
210
|
+
location: u.location,
|
|
204
211
|
contact: {
|
|
205
212
|
phone_numbers: (u.phones ?? []).map((p) => ({
|
|
206
213
|
sanitized_number: p.normalizedNumber ?? p.number ?? "",
|
|
@@ -523,8 +530,7 @@ async function submitContactBulkChunk(
|
|
|
523
530
|
const cached = resp.cachedResults ?? [];
|
|
524
531
|
const seenIndexes = new Set<number>();
|
|
525
532
|
for (const hit of cached) {
|
|
526
|
-
const idx =
|
|
527
|
-
typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
533
|
+
const idx = typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
528
534
|
if (idx === undefined) continue;
|
|
529
535
|
seenIndexes.add(idx);
|
|
530
536
|
results.set(idx, {
|
|
@@ -684,7 +690,10 @@ export async function enrichPersonBulk(
|
|
|
684
690
|
return;
|
|
685
691
|
}
|
|
686
692
|
try {
|
|
687
|
-
validContacts.push({
|
|
693
|
+
validContacts.push({
|
|
694
|
+
inputIndex: index,
|
|
695
|
+
payload: buildContactPayload(body),
|
|
696
|
+
});
|
|
688
697
|
} catch (err) {
|
|
689
698
|
results.set(index, {
|
|
690
699
|
ok: false,
|
|
@@ -751,8 +760,7 @@ async function submitReverseEmailBulkChunk(
|
|
|
751
760
|
|
|
752
761
|
const cached = resp.cachedResults ?? [];
|
|
753
762
|
for (const hit of cached) {
|
|
754
|
-
const idx =
|
|
755
|
-
typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
763
|
+
const idx = typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
756
764
|
if (idx === undefined) continue;
|
|
757
765
|
// The gateway dedupes emails, so a cache hit at index i corresponds to
|
|
758
766
|
// the deduped email at that position in the request — we then fan out to
|
|
@@ -988,7 +996,9 @@ export async function enrichCompany(
|
|
|
988
996
|
body: EnrichCompanyBody,
|
|
989
997
|
options: EnrichmentGatewayOptions = {},
|
|
990
998
|
): Promise<Record<string, unknown>> {
|
|
991
|
-
const domain = body.domain
|
|
999
|
+
const domain = body.domain
|
|
1000
|
+
? (extractDomain(body.domain) ?? body.domain)
|
|
1001
|
+
: undefined;
|
|
992
1002
|
const companyName = body.companyName?.trim();
|
|
993
1003
|
const linkedinUrl = body.linkedinUrl?.trim();
|
|
994
1004
|
|
|
@@ -1075,7 +1085,11 @@ export async function aviatoEnrichPerson(
|
|
|
1075
1085
|
|
|
1076
1086
|
const resp = raw as {
|
|
1077
1087
|
data?: EnrichedPersonUpsertLike | null;
|
|
1078
|
-
billing?: {
|
|
1088
|
+
billing?: {
|
|
1089
|
+
charged_cents?: number;
|
|
1090
|
+
cache_hit?: boolean;
|
|
1091
|
+
preview?: boolean;
|
|
1092
|
+
};
|
|
1079
1093
|
provider?: { name?: string; aviato_id?: string };
|
|
1080
1094
|
};
|
|
1081
1095
|
|
|
@@ -1125,7 +1139,11 @@ export async function aviatoEnrichCompany(
|
|
|
1125
1139
|
|
|
1126
1140
|
const resp = raw as {
|
|
1127
1141
|
data?: EnrichedCompanyUpsertLike | null;
|
|
1128
|
-
billing?: {
|
|
1142
|
+
billing?: {
|
|
1143
|
+
charged_cents?: number;
|
|
1144
|
+
cache_hit?: boolean;
|
|
1145
|
+
preview?: boolean;
|
|
1146
|
+
};
|
|
1129
1147
|
provider?: { name?: string; aviato_id?: string };
|
|
1130
1148
|
};
|
|
1131
1149
|
|
|
@@ -1219,8 +1237,7 @@ async function callGatewayJson(
|
|
|
1219
1237
|
});
|
|
1220
1238
|
|
|
1221
1239
|
const isOk =
|
|
1222
|
-
response.ok ||
|
|
1223
|
-
(args.expectedStatus?.includes(response.status) ?? false);
|
|
1240
|
+
response.ok || (args.expectedStatus?.includes(response.status) ?? false);
|
|
1224
1241
|
|
|
1225
1242
|
if (!isOk) {
|
|
1226
1243
|
throw await buildGatewayError(response, pathOrUrl.toString());
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Dench agent workspace CLI.",
|
|
3
|
+
"version": "2.0.0",
|
|
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": {
|
|
7
7
|
"dench": "dench",
|