@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 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.dev` (production). Use `dench backend`
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 login` and caused the CLI to
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
- createAgentLoginRequest: makeFunctionReference<"mutation">(
168
- "functions/agentWorkspace:createAgentLoginRequest",
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
- pollAgentLoginRequest: makeFunctionReference<"mutation">(
192
- "functions/agentWorkspace:pollAgentLoginRequest",
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 login or set DENCH_API_KEY + CONVEX_URL.",
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 login for terminal access.",
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 login session instead.",
358
- "Or run dench login to create a new terminal session.",
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 onboard [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
427
- dench setup [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
428
- dench what-can-i-do [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
429
- dench login [--host ${DEFAULT_HOST}] [--name "Claude Code Agent"] [--kind <kind>] [--no-open]
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 onboard wraps login (when needed), workspace orientation, tasks, tools,
574
- suggested work, memory, and next recommended actions.
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 login approval, run dench status --mine --json and dench tasks --json.
580
- Use dench memory and dench artifacts to continue from durable context.
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 login
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 login\` agent session is required —
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 onboardHelp() {
673
- console.log(`Dench onboard
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 login [--name "AI Agent - Billing Repo"] [--kind <kind>] [--host <host>] [--no-open]
691
-
692
- Agent kind:
693
- --kind accepts any string. Defaults to "other" when omitted.
694
- Suggested values: claude_code, codex, cursor, hermes, openclaw, or any
695
- custom kind such as aider, goose, or some_custom_agent. Values are
696
- normalized to lowercase snake_case before send (e.g. "Claude Code"
697
- becomes claude_code, "my agent" becomes my_agent).
698
-
699
- Agent setup:
700
- Claude Code: dench login --kind claude_code --name "Claude Code Agent - Project"
701
- Codex: dench login --kind codex --name "Codex Agent - Project"
702
- Cursor: dench login --kind cursor --name "Cursor Agent - Project"
703
- Custom: dench login --kind some_custom_agent --name "My Agent - Project"
704
- After approval, run dench status --mine --json and dench tasks --json.
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 login\` agent session required. Alias
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 login".
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 login
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
- async function login() {
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
- const code = randomUrlSafe(18);
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 request = await client.mutation(
1555
- api.functions.agentWorkspace.createAgentLoginRequest,
1556
- {
1557
- codeHash,
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-login/${encodeURIComponent(code)}`;
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
- logHuman(
1573
- "Before approving, switch Dench in your browser to the intended workspace.",
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.pollAgentLoginRequest,
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 = loginNextCommandsMessage(
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(`Login ${poll.status}`);
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(`Login timed out. Open ${approvalUrl} and run login again.`);
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 login`, {
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 onboard --kind <kind> --name "AI Agent - Project" to mint an agent session.',
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 onboard to use a real agent session.",
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 login` setups keep their existing behavior.
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 onboard --kind <kind> --name "AI Agent - Project" to mint an agent session for this command.',
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 onboard --json",
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 register --name "AI Agent" --kind other --dev',
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 login output).",
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
- if (command === "login" && hasFlag("--help")) {
3186
- loginHelp();
3187
- return;
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 === "onboard" ||
3192
- command === "setup" ||
3193
- command === "what-can-i-do") &&
3685
+ (command === "signin" || isDeprecatedAuthCommand) &&
3194
3686
  (subcommand === "help" || hasFlag("--help"))
3195
3687
  ) {
3196
- onboardHelp();
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 login`) or a unified Dench API key
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 login`) or a unified Dench API key
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 login` session
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 === "login") {
3586
- await login();
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.dev";
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.dev.",
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.dev.`,
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
- emails?: Array<{ address?: string; normalizedAddress?: string; type?: string }>;
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({ inputIndex: index, payload: buildContactPayload(body) });
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 ? extractDomain(body.domain) ?? body.domain : undefined;
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?: { charged_cents?: number; cache_hit?: boolean; preview?: boolean };
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?: { charged_cents?: number; cache_hit?: boolean; preview?: boolean };
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.9",
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",