@dench.com/cli 0.4.9 → 2.0.1
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/crm.ts +60 -26
- package/dench.ts +870 -229
- package/host.ts +3 -3
- package/image.ts +49 -40
- package/lib/enrichment-gateway.ts +30 -13
- package/package.json +2 -2
- package/search.ts +56 -48
- package/tools.ts +59 -34
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,
|
|
@@ -970,11 +966,12 @@ function backendSnapshot(config: ConfigFile) {
|
|
|
970
966
|
: null;
|
|
971
967
|
const envHost = process.env.DENCH_HOST?.trim() || null;
|
|
972
968
|
const argBackend =
|
|
973
|
-
option("--backend") ??
|
|
969
|
+
option("--backend") ??
|
|
970
|
+
(hasFlag("--prod")
|
|
974
971
|
? "production"
|
|
975
972
|
: hasFlag("--staging")
|
|
976
973
|
? "staging"
|
|
977
|
-
: option("--host") ?? null);
|
|
974
|
+
: (option("--host") ?? null));
|
|
978
975
|
|
|
979
976
|
let source: "flag" | "env" | "config" | "default";
|
|
980
977
|
if (argBackend) source = "flag";
|
|
@@ -1082,7 +1079,9 @@ async function backendSet() {
|
|
|
1082
1079
|
"Local backend selected. dench will now also load .env / .env.local from the workspace so GATEWAY_URL etc. are picked up.",
|
|
1083
1080
|
);
|
|
1084
1081
|
}
|
|
1085
|
-
print(
|
|
1082
|
+
print(
|
|
1083
|
+
"Future `dench` commands without --prod/--staging/--host will target this backend.",
|
|
1084
|
+
);
|
|
1086
1085
|
}
|
|
1087
1086
|
|
|
1088
1087
|
async function backendClear() {
|
|
@@ -1539,29 +1538,183 @@ async function discoverConvexUrl(host: string) {
|
|
|
1539
1538
|
return payload.convexUrl;
|
|
1540
1539
|
}
|
|
1541
1540
|
|
|
1542
|
-
|
|
1541
|
+
/**
|
|
1542
|
+
* Read a single line from stdin. Used by `dench signin --email` for
|
|
1543
|
+
* the OTP code and (when the user has multiple admin workspaces) the
|
|
1544
|
+
* org-picker prompt. Returns the trimmed line or throws if stdin
|
|
1545
|
+
* isn't a TTY — callers can pre-supply the value via flags to avoid
|
|
1546
|
+
* the prompt in CI / non-interactive shells.
|
|
1547
|
+
*/
|
|
1548
|
+
async function promptLine(question: string): Promise<string> {
|
|
1549
|
+
if (!process.stdin.isTTY) {
|
|
1550
|
+
throw new CliError(
|
|
1551
|
+
`Non-interactive shell: pass the answer to "${question}" via a CLI flag instead of prompting.`,
|
|
1552
|
+
{ code: "stdin_not_tty" },
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
process.stdout.write(question);
|
|
1556
|
+
return await new Promise<string>((resolve, reject) => {
|
|
1557
|
+
let data = "";
|
|
1558
|
+
const onData = (chunk: Buffer | string) => {
|
|
1559
|
+
data += chunk.toString();
|
|
1560
|
+
if (data.includes("\n")) {
|
|
1561
|
+
cleanup();
|
|
1562
|
+
resolve(data.trim());
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
const onErr = (err: Error) => {
|
|
1566
|
+
cleanup();
|
|
1567
|
+
reject(err);
|
|
1568
|
+
};
|
|
1569
|
+
const cleanup = () => {
|
|
1570
|
+
process.stdin.removeListener("data", onData);
|
|
1571
|
+
process.stdin.removeListener("error", onErr);
|
|
1572
|
+
process.stdin.pause();
|
|
1573
|
+
};
|
|
1574
|
+
process.stdin.resume();
|
|
1575
|
+
process.stdin.on("data", onData);
|
|
1576
|
+
process.stdin.on("error", onErr);
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
type SignInIntent = "join_existing" | "create_workspace";
|
|
1581
|
+
|
|
1582
|
+
function signinNextCommandsMessage(
|
|
1583
|
+
session: StoredSession,
|
|
1584
|
+
stableSessionKey: string | undefined,
|
|
1585
|
+
) {
|
|
1586
|
+
const workspaceName = session.organization?.name ?? "unknown workspace";
|
|
1587
|
+
const workspaceSlug = session.organization?.slug ?? "unknown";
|
|
1588
|
+
const agentName = session.agent?.name ?? "unknown agent";
|
|
1589
|
+
const stableKeyText = stableSessionKey
|
|
1590
|
+
? ` Stable session key: ${stableSessionKey}.`
|
|
1591
|
+
: "";
|
|
1592
|
+
return `Signed in. Next commands will use workspace ${workspaceName} (${workspaceSlug}) as ${agentName}.${stableKeyText}`;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* `dench signin` — the single unified CLI auth entry point.
|
|
1597
|
+
*
|
|
1598
|
+
* Two transports (browser link or OTP) × two intents (join existing
|
|
1599
|
+
* workspace or create a new free CRM-only workspace). Replaces the
|
|
1600
|
+
* deprecated `dench login`, `dench onboard`, `dench setup`,
|
|
1601
|
+
* `dench what-can-i-do`, and `dench register` commands.
|
|
1602
|
+
*
|
|
1603
|
+
* Flag matrix:
|
|
1604
|
+
*
|
|
1605
|
+
* (no flags) Browser link, join existing.
|
|
1606
|
+
* --new-workspace --org-name "X" Browser link, create new free workspace.
|
|
1607
|
+
* --email <addr> OTP flow, join existing.
|
|
1608
|
+
* --email + --new-workspace ... OTP flow, create new free workspace.
|
|
1609
|
+
* --org <slug-or-id> Skip the org picker (existing workspaces).
|
|
1610
|
+
*
|
|
1611
|
+
* Both transports save the resulting `dch_agent_*` session to
|
|
1612
|
+
* `~/.dench/config.json` via `saveSession()` — identical shape to the
|
|
1613
|
+
* previous login flow, so `dench logout` / `dench use` keep working.
|
|
1614
|
+
*/
|
|
1615
|
+
async function signin() {
|
|
1543
1616
|
const scope = resolveSessionScope({ args: filteredArgs });
|
|
1544
1617
|
const explicitKey = explicitSessionKeyInput({ args: filteredArgs });
|
|
1545
1618
|
const host = resolveHostFromArgs(filteredArgs, undefined);
|
|
1546
1619
|
const convexUrl = await discoverConvexUrl(host);
|
|
1547
1620
|
const client = new ConvexHttpClient(convexUrl);
|
|
1548
|
-
|
|
1621
|
+
|
|
1549
1622
|
const sessionToken = `dch_agent_${randomUrlSafe(32)}`;
|
|
1550
|
-
const codeHash = await sha256Hex(code);
|
|
1551
1623
|
const sessionTokenHash = await sha256Hex(sessionToken);
|
|
1624
|
+
|
|
1552
1625
|
const agentKind = normalizeAgentKind(option("--kind"));
|
|
1553
1626
|
const agentName = resolveAgentName(agentKind);
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1627
|
+
const email = option("--email")?.trim().toLowerCase() || null;
|
|
1628
|
+
const newWorkspace = hasFlag("--new-workspace");
|
|
1629
|
+
const orgNameFlag = option("--org-name")?.trim() || null;
|
|
1630
|
+
const orgFlag = option("--org")?.trim() || null;
|
|
1631
|
+
|
|
1632
|
+
if (newWorkspace && !orgNameFlag) {
|
|
1633
|
+
throw new CliError(
|
|
1634
|
+
"--new-workspace requires --org-name '<workspace name>'.",
|
|
1635
|
+
{ code: "missing_org_name" },
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
if (newWorkspace && orgFlag) {
|
|
1639
|
+
throw new CliError(
|
|
1640
|
+
"Pass either --new-workspace --org-name '<name>' OR --org <slug>, not both.",
|
|
1641
|
+
{ code: "conflicting_intent" },
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const intent: SignInIntent = newWorkspace
|
|
1646
|
+
? "create_workspace"
|
|
1647
|
+
: "join_existing";
|
|
1648
|
+
|
|
1649
|
+
if (email) {
|
|
1650
|
+
await signinViaOtp({
|
|
1651
|
+
host,
|
|
1652
|
+
scope,
|
|
1653
|
+
explicitKey,
|
|
1654
|
+
sessionToken,
|
|
1558
1655
|
sessionTokenHash,
|
|
1559
1656
|
agentName,
|
|
1560
1657
|
agentKind,
|
|
1658
|
+
email,
|
|
1659
|
+
intent,
|
|
1660
|
+
proposedOrganizationName: orgNameFlag,
|
|
1661
|
+
orgSelector: orgFlag,
|
|
1662
|
+
});
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
await signinViaBrowser({
|
|
1667
|
+
client,
|
|
1668
|
+
host,
|
|
1669
|
+
convexUrl,
|
|
1670
|
+
scope,
|
|
1671
|
+
explicitKey,
|
|
1672
|
+
sessionToken,
|
|
1673
|
+
sessionTokenHash,
|
|
1674
|
+
agentName,
|
|
1675
|
+
agentKind,
|
|
1676
|
+
intent,
|
|
1677
|
+
proposedOrganizationName: orgNameFlag,
|
|
1678
|
+
orgSelector: orgFlag,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Browser-link variant of `dench signin`. Creates a sign-in request
|
|
1684
|
+
* row in Convex, opens the approval URL in the user's browser, then
|
|
1685
|
+
* polls until the human approves the request (and picks/creates the
|
|
1686
|
+
* target workspace on the approval page).
|
|
1687
|
+
*/
|
|
1688
|
+
async function signinViaBrowser(input: {
|
|
1689
|
+
client: ConvexHttpClient;
|
|
1690
|
+
host: string;
|
|
1691
|
+
convexUrl: string;
|
|
1692
|
+
scope: SessionScope;
|
|
1693
|
+
explicitKey: ReturnType<typeof explicitSessionKeyInput>;
|
|
1694
|
+
sessionToken: string;
|
|
1695
|
+
sessionTokenHash: string;
|
|
1696
|
+
agentName: string;
|
|
1697
|
+
agentKind: string;
|
|
1698
|
+
intent: SignInIntent;
|
|
1699
|
+
proposedOrganizationName: string | null;
|
|
1700
|
+
orgSelector: string | null;
|
|
1701
|
+
}) {
|
|
1702
|
+
const code = randomUrlSafe(18);
|
|
1703
|
+
const codeHash = await sha256Hex(code);
|
|
1704
|
+
const request = await input.client.mutation(
|
|
1705
|
+
api.functions.agentWorkspace.createSignInRequest,
|
|
1706
|
+
{
|
|
1707
|
+
codeHash,
|
|
1708
|
+
sessionTokenHash: input.sessionTokenHash,
|
|
1709
|
+
agentName: input.agentName,
|
|
1710
|
+
agentKind: input.agentKind,
|
|
1711
|
+
intent: input.intent,
|
|
1712
|
+
proposedOrganizationName: input.proposedOrganizationName ?? undefined,
|
|
1713
|
+
requestedOrganizationSelector: input.orgSelector ?? undefined,
|
|
1561
1714
|
userAgent: "@dench.com/cli",
|
|
1562
1715
|
},
|
|
1563
1716
|
);
|
|
1564
|
-
const approvalUrl = `${host}/agent-
|
|
1717
|
+
const approvalUrl = `${input.host}/agent-signin/${encodeURIComponent(code)}`;
|
|
1565
1718
|
|
|
1566
1719
|
const openResult = await openUrl(approvalUrl, {
|
|
1567
1720
|
noOpen: hasFlag("--no-open"),
|
|
@@ -1569,9 +1722,19 @@ async function login() {
|
|
|
1569
1722
|
env: process.env,
|
|
1570
1723
|
});
|
|
1571
1724
|
logHuman(formatApprovalOpenMessage(approvalUrl, openResult));
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1725
|
+
if (input.intent === "create_workspace") {
|
|
1726
|
+
logHuman(
|
|
1727
|
+
`On the approval page, confirm the workspace name and connect ${input.agentName}.`,
|
|
1728
|
+
);
|
|
1729
|
+
} else if (input.orgSelector) {
|
|
1730
|
+
logHuman(
|
|
1731
|
+
`On the approval page, confirm connecting ${input.agentName} to ${input.orgSelector}.`,
|
|
1732
|
+
);
|
|
1733
|
+
} else {
|
|
1734
|
+
logHuman(
|
|
1735
|
+
`On the approval page, pick which workspace to connect ${input.agentName} to (or create a new one).`,
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1575
1738
|
logHuman(
|
|
1576
1739
|
`This request expires at ${new Date(request.expiresAt).toLocaleString()}.`,
|
|
1577
1740
|
);
|
|
@@ -1579,33 +1742,33 @@ async function login() {
|
|
|
1579
1742
|
const timeoutMs = Number(option("--timeout-ms") ?? 15 * 60 * 1000);
|
|
1580
1743
|
const deadline = Date.now() + timeoutMs;
|
|
1581
1744
|
while (Date.now() < deadline) {
|
|
1582
|
-
const poll = await client.mutation(
|
|
1583
|
-
api.functions.agentWorkspace.
|
|
1584
|
-
{ codeHash, sessionTokenHash },
|
|
1745
|
+
const poll = await input.client.mutation(
|
|
1746
|
+
api.functions.agentWorkspace.pollSignInRequest,
|
|
1747
|
+
{ codeHash, sessionTokenHash: input.sessionTokenHash },
|
|
1585
1748
|
);
|
|
1586
1749
|
|
|
1587
1750
|
if (poll.status === "approved") {
|
|
1588
1751
|
const storedSession: StoredSession = {
|
|
1589
|
-
host,
|
|
1590
|
-
convexUrl,
|
|
1591
|
-
sessionToken,
|
|
1752
|
+
host: input.host,
|
|
1753
|
+
convexUrl: input.convexUrl,
|
|
1754
|
+
sessionToken: input.sessionToken,
|
|
1592
1755
|
organization: poll.organization,
|
|
1593
1756
|
agent: poll.agent,
|
|
1594
1757
|
sessionExpiresAt: poll.sessionExpiresAt,
|
|
1595
1758
|
savedAt: Date.now(),
|
|
1596
1759
|
};
|
|
1597
|
-
await saveSession(storedSession, scope);
|
|
1598
|
-
const message =
|
|
1760
|
+
await saveSession(storedSession, input.scope);
|
|
1761
|
+
const message = signinNextCommandsMessage(
|
|
1599
1762
|
storedSession,
|
|
1600
|
-
explicitKey?.source === "DENCH_SESSION_KEY"
|
|
1601
|
-
? explicitKey.value
|
|
1763
|
+
input.explicitKey?.source === "DENCH_SESSION_KEY"
|
|
1764
|
+
? input.explicitKey.value
|
|
1602
1765
|
: undefined,
|
|
1603
1766
|
);
|
|
1604
1767
|
print(
|
|
1605
1768
|
json
|
|
1606
1769
|
? {
|
|
1607
1770
|
ok: true,
|
|
1608
|
-
host,
|
|
1771
|
+
host: input.host,
|
|
1609
1772
|
organization: poll.organization,
|
|
1610
1773
|
agent: poll.agent,
|
|
1611
1774
|
sessionExpiresAt: poll.sessionExpiresAt,
|
|
@@ -1616,13 +1779,275 @@ async function login() {
|
|
|
1616
1779
|
return;
|
|
1617
1780
|
}
|
|
1618
1781
|
if (poll.status === "rejected" || poll.status === "expired") {
|
|
1619
|
-
throw new Error(`
|
|
1782
|
+
throw new Error(`Sign-in ${poll.status}`);
|
|
1620
1783
|
}
|
|
1621
1784
|
|
|
1622
1785
|
await sleep(2000);
|
|
1623
1786
|
}
|
|
1624
1787
|
|
|
1625
|
-
throw new Error(
|
|
1788
|
+
throw new Error(
|
|
1789
|
+
`Sign-in timed out. Open ${approvalUrl} and run \`dench signin\` again.`,
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
type OtpVerifyResult =
|
|
1794
|
+
| {
|
|
1795
|
+
kind: "completed";
|
|
1796
|
+
organization?: { id: string; name: string; slug: string };
|
|
1797
|
+
sessionExpiresAt?: number;
|
|
1798
|
+
}
|
|
1799
|
+
| {
|
|
1800
|
+
kind: "requiresOrgChoice";
|
|
1801
|
+
authToken: string;
|
|
1802
|
+
organizations: Array<{ id: string; name: string; slug: string }>;
|
|
1803
|
+
expiresAt: number;
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
/**
|
|
1807
|
+
* OTP variant of `dench signin`. Sends the OTP via the Next.js
|
|
1808
|
+
* agent-signin routes, prompts the human for the 6-digit code, and
|
|
1809
|
+
* mints a session against the org they pick (or create) in one or
|
|
1810
|
+
* two HTTP round-trips.
|
|
1811
|
+
*/
|
|
1812
|
+
async function signinViaOtp(input: {
|
|
1813
|
+
host: string;
|
|
1814
|
+
scope: SessionScope;
|
|
1815
|
+
explicitKey: ReturnType<typeof explicitSessionKeyInput>;
|
|
1816
|
+
sessionToken: string;
|
|
1817
|
+
sessionTokenHash: string;
|
|
1818
|
+
agentName: string;
|
|
1819
|
+
agentKind: string;
|
|
1820
|
+
email: string;
|
|
1821
|
+
intent: SignInIntent;
|
|
1822
|
+
proposedOrganizationName: string | null;
|
|
1823
|
+
orgSelector: string | null;
|
|
1824
|
+
}) {
|
|
1825
|
+
const base = input.host.replace(/\/+$/, "");
|
|
1826
|
+
|
|
1827
|
+
// Step 1 — send the OTP.
|
|
1828
|
+
const startResponse = await fetch(`${base}/api/agent-signin/otp-start`, {
|
|
1829
|
+
method: "POST",
|
|
1830
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1831
|
+
body: JSON.stringify({ email: input.email }),
|
|
1832
|
+
});
|
|
1833
|
+
if (!startResponse.ok) {
|
|
1834
|
+
const body = await startResponse.text();
|
|
1835
|
+
throw new CliError(
|
|
1836
|
+
`OTP send failed (${startResponse.status}): ${body || "unknown error"}`,
|
|
1837
|
+
{ code: "otp_send_failed", status: startResponse.status },
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
logHuman(`We sent a 6-digit sign-in code to ${input.email}.`);
|
|
1841
|
+
|
|
1842
|
+
// Step 2 — verify the OTP. Either the user is unambiguously
|
|
1843
|
+
// resolvable (single admin org or create_workspace intent) and the
|
|
1844
|
+
// server hands us a session, OR we get an auth token + list and
|
|
1845
|
+
// need a third call.
|
|
1846
|
+
const code =
|
|
1847
|
+
option("--otp-code")?.trim() || (await promptLine("Paste OTP: "));
|
|
1848
|
+
if (!/^\d{6}$/.test(code)) {
|
|
1849
|
+
throw new CliError("OTP code must be exactly 6 digits.", {
|
|
1850
|
+
code: "invalid_otp_code",
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
const verifyResponse = await fetch(`${base}/api/agent-signin/otp-verify`, {
|
|
1854
|
+
method: "POST",
|
|
1855
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1856
|
+
body: JSON.stringify({
|
|
1857
|
+
email: input.email,
|
|
1858
|
+
code,
|
|
1859
|
+
agentName: input.agentName,
|
|
1860
|
+
agentKind: input.agentKind,
|
|
1861
|
+
sessionTokenHash: input.sessionTokenHash,
|
|
1862
|
+
intent: input.intent,
|
|
1863
|
+
proposedOrganizationName: input.proposedOrganizationName ?? undefined,
|
|
1864
|
+
organizationSelector: input.orgSelector ?? undefined,
|
|
1865
|
+
userAgent: "@dench.com/cli",
|
|
1866
|
+
}),
|
|
1867
|
+
});
|
|
1868
|
+
if (!verifyResponse.ok) {
|
|
1869
|
+
const body = await verifyResponse.text();
|
|
1870
|
+
throw new CliError(
|
|
1871
|
+
`OTP verify failed (${verifyResponse.status}): ${body || "unknown error"}`,
|
|
1872
|
+
{ code: "otp_verify_failed", status: verifyResponse.status },
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
const verifyPayload = (await verifyResponse.json()) as {
|
|
1876
|
+
ok: boolean;
|
|
1877
|
+
result: OtpVerifyResult;
|
|
1878
|
+
};
|
|
1879
|
+
const result = verifyPayload.result;
|
|
1880
|
+
|
|
1881
|
+
// Branch A: server minted the session in one shot.
|
|
1882
|
+
if (result.kind === "completed") {
|
|
1883
|
+
assertOtpCompletedOrgMatchesSelector(
|
|
1884
|
+
result.organization,
|
|
1885
|
+
input.orgSelector,
|
|
1886
|
+
);
|
|
1887
|
+
await finalizeOtpSession(
|
|
1888
|
+
input,
|
|
1889
|
+
result.organization,
|
|
1890
|
+
result.sessionExpiresAt,
|
|
1891
|
+
);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Branch B: human has multiple admin orgs — pick one (or create new).
|
|
1896
|
+
const finalizeChoice = await resolveOrgChoiceForOtp({
|
|
1897
|
+
organizations: result.organizations,
|
|
1898
|
+
orgSelector: input.orgSelector,
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
const finalizeResponse = await fetch(
|
|
1902
|
+
`${base}/api/agent-signin/otp-finalize`,
|
|
1903
|
+
{
|
|
1904
|
+
method: "POST",
|
|
1905
|
+
headers: {
|
|
1906
|
+
"content-type": "application/json",
|
|
1907
|
+
accept: "application/json",
|
|
1908
|
+
},
|
|
1909
|
+
body: JSON.stringify({
|
|
1910
|
+
authToken: result.authToken,
|
|
1911
|
+
organizationId: finalizeChoice.organizationId,
|
|
1912
|
+
newOrganizationName: finalizeChoice.newOrganizationName,
|
|
1913
|
+
}),
|
|
1914
|
+
},
|
|
1915
|
+
);
|
|
1916
|
+
if (!finalizeResponse.ok) {
|
|
1917
|
+
const body = await finalizeResponse.text();
|
|
1918
|
+
throw new CliError(
|
|
1919
|
+
`OTP finalize failed (${finalizeResponse.status}): ${body || "unknown error"}`,
|
|
1920
|
+
{ code: "otp_finalize_failed", status: finalizeResponse.status },
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const finalizePayload = (await finalizeResponse.json()) as {
|
|
1924
|
+
ok: boolean;
|
|
1925
|
+
result: {
|
|
1926
|
+
organization?: { id: string; name: string; slug: string };
|
|
1927
|
+
sessionExpiresAt?: number;
|
|
1928
|
+
};
|
|
1929
|
+
};
|
|
1930
|
+
await finalizeOtpSession(
|
|
1931
|
+
input,
|
|
1932
|
+
finalizePayload.result.organization,
|
|
1933
|
+
finalizePayload.result.sessionExpiresAt,
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function assertOtpCompletedOrgMatchesSelector(
|
|
1938
|
+
organization: { id: string; name: string; slug: string } | undefined,
|
|
1939
|
+
orgSelector: string | null,
|
|
1940
|
+
) {
|
|
1941
|
+
if (!orgSelector) return;
|
|
1942
|
+
const normalized = orgSelector.trim().toLowerCase();
|
|
1943
|
+
if (!organization) {
|
|
1944
|
+
throw new CliError(
|
|
1945
|
+
`The OTP flow completed without an organization, but --org ${orgSelector} was requested.`,
|
|
1946
|
+
{ code: "org_not_found" },
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
const matches =
|
|
1950
|
+
organization.id.toLowerCase() === normalized ||
|
|
1951
|
+
organization.slug.toLowerCase() === normalized ||
|
|
1952
|
+
organization.name.trim().toLowerCase() === normalized;
|
|
1953
|
+
if (!matches) {
|
|
1954
|
+
throw new CliError(
|
|
1955
|
+
`The OTP flow resolved ${organization.name} (${organization.slug}), but --org ${orgSelector} was requested.`,
|
|
1956
|
+
{ code: "org_mismatch" },
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
async function finalizeOtpSession(
|
|
1962
|
+
input: {
|
|
1963
|
+
host: string;
|
|
1964
|
+
scope: SessionScope;
|
|
1965
|
+
explicitKey: ReturnType<typeof explicitSessionKeyInput>;
|
|
1966
|
+
sessionToken: string;
|
|
1967
|
+
},
|
|
1968
|
+
organization: { id: string; name: string; slug: string } | undefined,
|
|
1969
|
+
sessionExpiresAt: number | undefined,
|
|
1970
|
+
) {
|
|
1971
|
+
const convexUrl = await discoverConvexUrl(input.host);
|
|
1972
|
+
const storedSession: StoredSession = {
|
|
1973
|
+
host: input.host,
|
|
1974
|
+
convexUrl,
|
|
1975
|
+
sessionToken: input.sessionToken,
|
|
1976
|
+
organization,
|
|
1977
|
+
sessionExpiresAt,
|
|
1978
|
+
savedAt: Date.now(),
|
|
1979
|
+
};
|
|
1980
|
+
await saveSession(storedSession, input.scope);
|
|
1981
|
+
const message = signinNextCommandsMessage(
|
|
1982
|
+
storedSession,
|
|
1983
|
+
input.explicitKey?.source === "DENCH_SESSION_KEY"
|
|
1984
|
+
? input.explicitKey.value
|
|
1985
|
+
: undefined,
|
|
1986
|
+
);
|
|
1987
|
+
print(
|
|
1988
|
+
json
|
|
1989
|
+
? {
|
|
1990
|
+
ok: true,
|
|
1991
|
+
host: input.host,
|
|
1992
|
+
organization,
|
|
1993
|
+
sessionExpiresAt,
|
|
1994
|
+
message,
|
|
1995
|
+
}
|
|
1996
|
+
: message,
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
async function resolveOrgChoiceForOtp(input: {
|
|
2001
|
+
organizations: Array<{ id: string; name: string; slug: string }>;
|
|
2002
|
+
orgSelector: string | null;
|
|
2003
|
+
}): Promise<{ organizationId?: string; newOrganizationName?: string }> {
|
|
2004
|
+
if (input.orgSelector) {
|
|
2005
|
+
const match = input.organizations.find(
|
|
2006
|
+
(org) =>
|
|
2007
|
+
org.id === input.orgSelector ||
|
|
2008
|
+
org.slug === input.orgSelector ||
|
|
2009
|
+
org.name === input.orgSelector,
|
|
2010
|
+
);
|
|
2011
|
+
if (!match) {
|
|
2012
|
+
throw new CliError(
|
|
2013
|
+
`--org "${input.orgSelector}" did not match any of your admin workspaces.`,
|
|
2014
|
+
{ code: "org_not_found" },
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
return { organizationId: match.id };
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Interactive picker.
|
|
2021
|
+
logHuman("");
|
|
2022
|
+
logHuman("Which workspace should this agent connect to?");
|
|
2023
|
+
for (let i = 0; i < input.organizations.length; i++) {
|
|
2024
|
+
const org = input.organizations[i];
|
|
2025
|
+
logHuman(` ${i + 1}) ${org.name} (${org.slug})`);
|
|
2026
|
+
}
|
|
2027
|
+
logHuman(` ${input.organizations.length + 1}) Create a new free workspace`);
|
|
2028
|
+
logHuman("");
|
|
2029
|
+
|
|
2030
|
+
const answer = await promptLine("Pick a number: ");
|
|
2031
|
+
const choice = Number(answer);
|
|
2032
|
+
if (
|
|
2033
|
+
!Number.isFinite(choice) ||
|
|
2034
|
+
choice < 1 ||
|
|
2035
|
+
choice > input.organizations.length + 1
|
|
2036
|
+
) {
|
|
2037
|
+
throw new CliError(`Invalid choice: ${answer}`, {
|
|
2038
|
+
code: "invalid_org_choice",
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
if (choice <= input.organizations.length) {
|
|
2042
|
+
return { organizationId: input.organizations[choice - 1].id };
|
|
2043
|
+
}
|
|
2044
|
+
const newName = await promptLine("New workspace name: ");
|
|
2045
|
+
if (!newName.trim()) {
|
|
2046
|
+
throw new CliError("Workspace name cannot be empty.", {
|
|
2047
|
+
code: "missing_org_name",
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
return { newOrganizationName: newName.trim() };
|
|
1626
2051
|
}
|
|
1627
2052
|
|
|
1628
2053
|
function normalizeApiBase(input: string) {
|
|
@@ -1745,6 +2170,114 @@ async function billingTopup(runtime: Runtime) {
|
|
|
1745
2170
|
print(json ? output : output.message);
|
|
1746
2171
|
}
|
|
1747
2172
|
|
|
2173
|
+
/**
|
|
2174
|
+
* `dench upgrade` — turns a free CRM-only workspace into a paid Pro or
|
|
2175
|
+
* Max plan by handing the human a Stripe Checkout link. Mirrors the
|
|
2176
|
+
* `billing topup` flow (POST with the agent session as a Bearer
|
|
2177
|
+
* token, surface the returned URL) but hits the existing
|
|
2178
|
+
* `/api/stripe/create-checkout` endpoint that the marketing pricing
|
|
2179
|
+
* page already uses.
|
|
2180
|
+
*
|
|
2181
|
+
* Two flag-driven paths:
|
|
2182
|
+
*
|
|
2183
|
+
* --tier pro | max Which paid tier to target. Defaults to
|
|
2184
|
+
* "pro" (the most common upgrade from
|
|
2185
|
+
* free).
|
|
2186
|
+
* --cycle monthly | yearly Billing cycle. Defaults to "monthly".
|
|
2187
|
+
*
|
|
2188
|
+
* The endpoint can also return `{ updated: true }` for in-place
|
|
2189
|
+
* Pro \u2192 Max upgrades on an already-subscribed org (no Checkout URL
|
|
2190
|
+
* needed in that case). We surface either outcome cleanly.
|
|
2191
|
+
*/
|
|
2192
|
+
async function denchUpgrade(runtime: Runtime) {
|
|
2193
|
+
const sessionRuntime = requireSessionRuntime(runtime, "dench upgrade");
|
|
2194
|
+
const tierFlag = option("--tier")?.trim().toLowerCase() || "pro";
|
|
2195
|
+
const cycleFlag =
|
|
2196
|
+
option("--cycle")?.trim().toLowerCase() ||
|
|
2197
|
+
option("--billing-cycle")?.trim().toLowerCase() ||
|
|
2198
|
+
"monthly";
|
|
2199
|
+
if (tierFlag !== "pro" && tierFlag !== "max") {
|
|
2200
|
+
throw new CliError("--tier must be 'pro' or 'max'.", {
|
|
2201
|
+
code: "invalid_tier",
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
if (cycleFlag !== "monthly" && cycleFlag !== "yearly") {
|
|
2205
|
+
throw new CliError("--cycle must be 'monthly' or 'yearly'.", {
|
|
2206
|
+
code: "invalid_cycle",
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
// Internal tier strings still use the legacy "desktop" / "cloud"
|
|
2210
|
+
// names from before the Daytona rewrite. Map the CLI's
|
|
2211
|
+
// customer-facing flag onto them so the existing checkout endpoint
|
|
2212
|
+
// doesn't need to learn a new vocabulary.
|
|
2213
|
+
const internalTier = tierFlag === "pro" ? "desktop" : "cloud";
|
|
2214
|
+
|
|
2215
|
+
const response = await fetch(
|
|
2216
|
+
`${sessionRuntime.host.replace(/\/+$/, "")}/api/stripe/create-checkout`,
|
|
2217
|
+
{
|
|
2218
|
+
method: "POST",
|
|
2219
|
+
headers: {
|
|
2220
|
+
accept: "application/json",
|
|
2221
|
+
"content-type": "application/json",
|
|
2222
|
+
authorization: `Bearer ${sessionRuntime.sessionToken}`,
|
|
2223
|
+
},
|
|
2224
|
+
body: JSON.stringify({
|
|
2225
|
+
tier: internalTier,
|
|
2226
|
+
billingCycle: cycleFlag,
|
|
2227
|
+
requestId: crypto.randomUUID(),
|
|
2228
|
+
}),
|
|
2229
|
+
},
|
|
2230
|
+
);
|
|
2231
|
+
const payload = await parseResponseBody(response);
|
|
2232
|
+
if (!response.ok) {
|
|
2233
|
+
const errorRecord = asRecord(payload);
|
|
2234
|
+
throw new CliError(
|
|
2235
|
+
stringField(errorRecord, "error", "message") ??
|
|
2236
|
+
`Could not create upgrade checkout (${response.status})`,
|
|
2237
|
+
{
|
|
2238
|
+
code: stringField(errorRecord, "code") ?? "upgrade_failed",
|
|
2239
|
+
status: response.status,
|
|
2240
|
+
},
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
const record = asRecord(payload);
|
|
2244
|
+
const url = stringField(record, "url");
|
|
2245
|
+
const updatedInPlace = record?.updated === true;
|
|
2246
|
+
|
|
2247
|
+
if (updatedInPlace) {
|
|
2248
|
+
const message = `Upgraded to Dench ${tierFlag === "pro" ? "Pro" : "Max"} in place — no Stripe Checkout needed.`;
|
|
2249
|
+
print(
|
|
2250
|
+
json ? { ok: true, updated: true, tier: tierFlag, message } : message,
|
|
2251
|
+
);
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
const openResult = url
|
|
2256
|
+
? await openUrl(url, {
|
|
2257
|
+
noOpen: hasFlag("--no-open"),
|
|
2258
|
+
json,
|
|
2259
|
+
env: process.env,
|
|
2260
|
+
})
|
|
2261
|
+
: ({ status: "skipped", reason: "missing-url" } as const);
|
|
2262
|
+
const openMessage =
|
|
2263
|
+
url && openResult.status === "opened"
|
|
2264
|
+
? `Attempted to open Stripe Checkout in your browser. If it did not open, use this link: ${url}`
|
|
2265
|
+
: url && openResult.status === "failed"
|
|
2266
|
+
? `Could not open browser automatically. Open this Stripe Checkout link: ${url}`
|
|
2267
|
+
: url
|
|
2268
|
+
? `Open this Stripe Checkout link: ${url}`
|
|
2269
|
+
: "Stripe Checkout link created.";
|
|
2270
|
+
const output = {
|
|
2271
|
+
ok: true,
|
|
2272
|
+
tier: tierFlag,
|
|
2273
|
+
billingCycle: cycleFlag,
|
|
2274
|
+
url,
|
|
2275
|
+
openResult,
|
|
2276
|
+
message: openMessage,
|
|
2277
|
+
};
|
|
2278
|
+
print(json ? output : output.message);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1748
2281
|
function hasDevEnv() {
|
|
1749
2282
|
return Boolean(
|
|
1750
2283
|
process.env.NEXT_PUBLIC_CONVEX_URL &&
|
|
@@ -1794,11 +2327,11 @@ function resolveApiHost(fallbackHost: string): string {
|
|
|
1794
2327
|
}
|
|
1795
2328
|
|
|
1796
2329
|
function loginRequiredError(command = "this command") {
|
|
1797
|
-
return new CliError(`${command} requires dench
|
|
2330
|
+
return new CliError(`${command} requires dench signin`, {
|
|
1798
2331
|
code: "login_required",
|
|
1799
2332
|
nextActions: [
|
|
1800
2333
|
"Set DENCH_API_KEY (and CONVEX_URL) in your env if running inside a sandbox or CI.",
|
|
1801
|
-
'Or run dench
|
|
2334
|
+
'Or run dench signin --kind <kind> --name "AI Agent - Project" to mint an agent session.',
|
|
1802
2335
|
"If already approved, run dench sessions --json, then dench use <session-key-or-workspace-slug>.",
|
|
1803
2336
|
],
|
|
1804
2337
|
});
|
|
@@ -1815,7 +2348,7 @@ function missingDevEnvError() {
|
|
|
1815
2348
|
missingEnv: required.filter((name) => !process.env[name]?.trim()),
|
|
1816
2349
|
nextActions: [
|
|
1817
2350
|
"Set NEXT_PUBLIC_CONVEX_URL, DENCH_WORKSPACE, and DENCH_DEV_AGENT_KEY.",
|
|
1818
|
-
"Or remove --dev and run dench
|
|
2351
|
+
"Or remove --dev and run dench signin to use a real agent session.",
|
|
1819
2352
|
],
|
|
1820
2353
|
});
|
|
1821
2354
|
}
|
|
@@ -1871,7 +2404,7 @@ async function getRuntime() {
|
|
|
1871
2404
|
// src/lib/secrets/sandbox-tokens.ts) is enough to drive every
|
|
1872
2405
|
// command that goes through `requireCrmAccess` server-side. We only
|
|
1873
2406
|
// fall into this branch when no stored session is present, so
|
|
1874
|
-
// explicit `dench
|
|
2407
|
+
// explicit `dench signin` setups keep their existing behavior.
|
|
1875
2408
|
const apiKey = process.env.DENCH_API_KEY?.trim();
|
|
1876
2409
|
const convexUrl = resolveApiKeyConvexUrl();
|
|
1877
2410
|
if (apiKey && convexUrl) {
|
|
@@ -1965,6 +2498,103 @@ function requireAuthenticatedRuntime(
|
|
|
1965
2498
|
throw loginRequiredError(command);
|
|
1966
2499
|
}
|
|
1967
2500
|
|
|
2501
|
+
type GatewayCommandAuth = {
|
|
2502
|
+
bearerToken: string;
|
|
2503
|
+
gatewayBaseUrl?: string;
|
|
2504
|
+
};
|
|
2505
|
+
|
|
2506
|
+
function gatewayBaseUrlForCli(value: unknown): string | undefined {
|
|
2507
|
+
if (typeof value !== "string") return undefined;
|
|
2508
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
2509
|
+
if (!trimmed) return undefined;
|
|
2510
|
+
// The desktop endpoint returns a `/v1` base; CLI gateway modules append
|
|
2511
|
+
// `/v1/...` paths themselves, so normalize to the root origin here.
|
|
2512
|
+
return trimmed.replace(/\/v1$/, "");
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
async function parseJsonResponseBody(response: Response): Promise<JsonRecord> {
|
|
2516
|
+
const text = await response.text().catch(() => "");
|
|
2517
|
+
if (!text.trim()) return {};
|
|
2518
|
+
try {
|
|
2519
|
+
const parsed = JSON.parse(text);
|
|
2520
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
2521
|
+
? (parsed as JsonRecord)
|
|
2522
|
+
: {};
|
|
2523
|
+
} catch {
|
|
2524
|
+
return { error: text };
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function gatewayAuthError(
|
|
2529
|
+
command: string,
|
|
2530
|
+
response: Response,
|
|
2531
|
+
body: JsonRecord,
|
|
2532
|
+
) {
|
|
2533
|
+
const code =
|
|
2534
|
+
typeof body.error === "string" && body.error.trim()
|
|
2535
|
+
? body.error.trim()
|
|
2536
|
+
: `gateway_key_${response.status}`;
|
|
2537
|
+
if (response.status === 403 && code === "no_active_subscription") {
|
|
2538
|
+
return new CliError(`${command} requires a paid Dench workspace`, {
|
|
2539
|
+
code: "paid_workspace_required",
|
|
2540
|
+
nextActions: [
|
|
2541
|
+
"Run dench upgrade --tier pro or dench upgrade --tier max, then retry.",
|
|
2542
|
+
],
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
if (response.status === 401) {
|
|
2546
|
+
return loginRequiredError(command);
|
|
2547
|
+
}
|
|
2548
|
+
return new CliError(`${command} could not get a gateway key (${code})`, {
|
|
2549
|
+
code,
|
|
2550
|
+
status: response.status,
|
|
2551
|
+
nextActions: [
|
|
2552
|
+
"Confirm this agent is signed in to the intended workspace.",
|
|
2553
|
+
"If the workspace is paid, retry after a minute in case gateway key provisioning is still finishing.",
|
|
2554
|
+
],
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
async function resolveGatewayCommandAuth(
|
|
2559
|
+
runtime: Runtime,
|
|
2560
|
+
command: string,
|
|
2561
|
+
): Promise<GatewayCommandAuth> {
|
|
2562
|
+
if (runtime.mode === "apiKey") {
|
|
2563
|
+
return { bearerToken: runtime.sessionToken };
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (runtime.mode === "session") {
|
|
2567
|
+
const response = await fetch(
|
|
2568
|
+
`${runtime.host.replace(/\/+$/, "")}/api/desktop/organizations/gateway-key`,
|
|
2569
|
+
{
|
|
2570
|
+
headers: {
|
|
2571
|
+
accept: "application/json",
|
|
2572
|
+
authorization: `Bearer ${runtime.sessionToken}`,
|
|
2573
|
+
},
|
|
2574
|
+
},
|
|
2575
|
+
);
|
|
2576
|
+
const body = await parseJsonResponseBody(response);
|
|
2577
|
+
if (!response.ok) {
|
|
2578
|
+
throw gatewayAuthError(command, response, body);
|
|
2579
|
+
}
|
|
2580
|
+
const gatewayKey =
|
|
2581
|
+
typeof body.gatewayKey === "string" ? body.gatewayKey.trim() : "";
|
|
2582
|
+
if (!gatewayKey) {
|
|
2583
|
+
throw new CliError(`${command} could not get a gateway key`, {
|
|
2584
|
+
code: "missing_gateway_key",
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
return {
|
|
2588
|
+
bearerToken: gatewayKey,
|
|
2589
|
+
gatewayBaseUrl: gatewayBaseUrlForCli(body.gatewayBaseUrl),
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
const envApiKey = process.env.DENCH_API_KEY?.trim();
|
|
2594
|
+
if (envApiKey) return { bearerToken: envApiKey };
|
|
2595
|
+
throw loginRequiredError(command);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
1968
2598
|
function encodeDevCrmSessionToken(args: {
|
|
1969
2599
|
workspaceSlug: string;
|
|
1970
2600
|
devKey: string;
|
|
@@ -1978,7 +2608,7 @@ function agentSessionRequiredError(command: string) {
|
|
|
1978
2608
|
{
|
|
1979
2609
|
code: "agent_session_required",
|
|
1980
2610
|
nextActions: [
|
|
1981
|
-
'Run dench
|
|
2611
|
+
'Run dench signin --kind <kind> --name "AI Agent - Project" to mint an agent session for this command.',
|
|
1982
2612
|
"Or run the equivalent action via the workspace UI / dench crm if it is org-level (e.g. CRM data, files).",
|
|
1983
2613
|
],
|
|
1984
2614
|
},
|
|
@@ -2079,7 +2709,7 @@ function workspaceUrls(host: string, workspaceSlug: string | undefined) {
|
|
|
2079
2709
|
|
|
2080
2710
|
function nextContextCommands() {
|
|
2081
2711
|
return [
|
|
2082
|
-
"dench
|
|
2712
|
+
"dench context --json",
|
|
2083
2713
|
"dench tasks --json",
|
|
2084
2714
|
'dench log "Brief progress update"',
|
|
2085
2715
|
'dench tool search "what you need" --toolkit <app>',
|
|
@@ -2087,12 +2717,24 @@ function nextContextCommands() {
|
|
|
2087
2717
|
];
|
|
2088
2718
|
}
|
|
2089
2719
|
|
|
2090
|
-
async function connectedAppsContext(
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2720
|
+
async function connectedAppsContext(runtime: Runtime) {
|
|
2721
|
+
try {
|
|
2722
|
+
const gatewayAuth = await resolveGatewayCommandAuth(
|
|
2723
|
+
runtime,
|
|
2724
|
+
"dench context",
|
|
2725
|
+
);
|
|
2726
|
+
return fetchConnectedAppsSummary({
|
|
2727
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
2728
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
2729
|
+
});
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
return {
|
|
2732
|
+
available: false,
|
|
2733
|
+
reason: errorMessage(error),
|
|
2734
|
+
count: 0,
|
|
2735
|
+
connections: [],
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2096
2738
|
}
|
|
2097
2739
|
|
|
2098
2740
|
async function buildContext(runtime: Runtime) {
|
|
@@ -2135,36 +2777,6 @@ async function buildContext(runtime: Runtime) {
|
|
|
2135
2777
|
};
|
|
2136
2778
|
}
|
|
2137
2779
|
|
|
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
2780
|
function formatCount(value: unknown) {
|
|
2169
2781
|
return typeof value === "number" ? String(value) : "0";
|
|
2170
2782
|
}
|
|
@@ -2441,7 +3053,7 @@ async function defaultDevAgentId(
|
|
|
2441
3053
|
const agent = data.agents[0];
|
|
2442
3054
|
if (!agent) {
|
|
2443
3055
|
throw new Error(
|
|
2444
|
-
'No agent registered yet. Run: dench
|
|
3056
|
+
'No agent registered yet. Run: dench signin --kind <kind> --name "AI Agent - Project"',
|
|
2445
3057
|
);
|
|
2446
3058
|
}
|
|
2447
3059
|
return agent._id;
|
|
@@ -2532,26 +3144,12 @@ function normalizeFilesPath(input: string): string {
|
|
|
2532
3144
|
return cleaned;
|
|
2533
3145
|
}
|
|
2534
3146
|
|
|
2535
|
-
function
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
const apiKey = filesApiKey(runtime);
|
|
2542
|
-
if (!apiKey) {
|
|
2543
|
-
throw new CliError(
|
|
2544
|
-
`${command} needs DENCH_API_KEY (or a sandbox-baked api-key session). It bypasses the user-session auth path.`,
|
|
2545
|
-
{
|
|
2546
|
-
code: "files_api_key_required",
|
|
2547
|
-
nextActions: [
|
|
2548
|
-
"Run inside a Dench sandbox where DENCH_API_KEY is baked in.",
|
|
2549
|
-
"Or export DENCH_API_KEY=<key> first (see dench login output).",
|
|
2550
|
-
],
|
|
2551
|
-
},
|
|
2552
|
-
);
|
|
2553
|
-
}
|
|
2554
|
-
return apiKey;
|
|
3147
|
+
async function requireFilesApiKey(
|
|
3148
|
+
runtime: Runtime,
|
|
3149
|
+
command: string,
|
|
3150
|
+
): Promise<string> {
|
|
3151
|
+
const auth = await resolveGatewayCommandAuth(runtime, command);
|
|
3152
|
+
return auth.bearerToken;
|
|
2555
3153
|
}
|
|
2556
3154
|
|
|
2557
3155
|
type FileTreeRow = {
|
|
@@ -2651,7 +3249,7 @@ async function uploadConvexFileBytes(
|
|
|
2651
3249
|
|
|
2652
3250
|
async function runFilesLs() {
|
|
2653
3251
|
const runtime = await getRuntime();
|
|
2654
|
-
const apiKey = requireFilesApiKey(runtime, "dench files ls");
|
|
3252
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files ls");
|
|
2655
3253
|
const target = normalizeFilesPath(positional(2) ?? "/");
|
|
2656
3254
|
const recursive = hasFlag("--recursive") || hasFlag("-r");
|
|
2657
3255
|
const rows = recursive
|
|
@@ -2688,7 +3286,7 @@ async function runFilesLs() {
|
|
|
2688
3286
|
|
|
2689
3287
|
async function runFilesMv() {
|
|
2690
3288
|
const runtime = await getRuntime();
|
|
2691
|
-
const apiKey = requireFilesApiKey(runtime, "dench files mv");
|
|
3289
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files mv");
|
|
2692
3290
|
const fromRaw = positional(2);
|
|
2693
3291
|
const toRaw = positional(3);
|
|
2694
3292
|
if (!fromRaw || !toRaw) {
|
|
@@ -2760,7 +3358,7 @@ async function runFilesMv() {
|
|
|
2760
3358
|
|
|
2761
3359
|
async function runFilesRm() {
|
|
2762
3360
|
const runtime = await getRuntime();
|
|
2763
|
-
const apiKey = requireFilesApiKey(runtime, "dench files rm");
|
|
3361
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files rm");
|
|
2764
3362
|
const targetRaw = positional(2);
|
|
2765
3363
|
if (!targetRaw) {
|
|
2766
3364
|
throw new CliError("Usage: dench files rm <path> [--recursive]", {
|
|
@@ -2947,7 +3545,7 @@ async function* walkLocalFiles(
|
|
|
2947
3545
|
|
|
2948
3546
|
async function runStage() {
|
|
2949
3547
|
const runtime = await getRuntime();
|
|
2950
|
-
const apiKey = requireFilesApiKey(runtime, "dench stage");
|
|
3548
|
+
const apiKey = await requireFilesApiKey(runtime, "dench stage");
|
|
2951
3549
|
const localRaw = positional(1);
|
|
2952
3550
|
const destRaw = positional(2);
|
|
2953
3551
|
if (!localRaw) {
|
|
@@ -3182,21 +3780,40 @@ async function main() {
|
|
|
3182
3780
|
return;
|
|
3183
3781
|
}
|
|
3184
3782
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3783
|
+
const isDeprecatedAuthCommand =
|
|
3784
|
+
command === "login" ||
|
|
3785
|
+
command === "onboard" ||
|
|
3786
|
+
command === "setup" ||
|
|
3787
|
+
command === "what-can-i-do" ||
|
|
3788
|
+
command === "register";
|
|
3189
3789
|
|
|
3190
3790
|
if (
|
|
3191
|
-
(command === "
|
|
3192
|
-
command === "setup" ||
|
|
3193
|
-
command === "what-can-i-do") &&
|
|
3791
|
+
(command === "signin" || isDeprecatedAuthCommand) &&
|
|
3194
3792
|
(subcommand === "help" || hasFlag("--help"))
|
|
3195
3793
|
) {
|
|
3196
|
-
|
|
3794
|
+
signinHelp();
|
|
3197
3795
|
return;
|
|
3198
3796
|
}
|
|
3199
3797
|
|
|
3798
|
+
// `dench login`, `dench onboard`, `dench setup`, `dench what-can-i-do`,
|
|
3799
|
+
// and `dench register` were removed in @dench.com/cli v2. Surface a
|
|
3800
|
+
// clean redirect to the new unified surface so old `npx` caches and
|
|
3801
|
+
// pre-existing agent prompts get a one-line nudge instead of an
|
|
3802
|
+
// opaque "Unknown command" error.
|
|
3803
|
+
if (isDeprecatedAuthCommand) {
|
|
3804
|
+
throw new CliError(
|
|
3805
|
+
`\`dench ${command}\` was removed in @dench.com/cli v2. Run \`dench signin\` instead.`,
|
|
3806
|
+
{
|
|
3807
|
+
code: "deprecated_command",
|
|
3808
|
+
nextActions: [
|
|
3809
|
+
"Run `dench signin` to connect this agent to an existing workspace.",
|
|
3810
|
+
'Run `dench signin --new-workspace --org-name "<name>"` to create a free CRM-only workspace.',
|
|
3811
|
+
"Upgrade the CLI globally with `npm install -g @dench.com/cli@latest`.",
|
|
3812
|
+
],
|
|
3813
|
+
},
|
|
3814
|
+
);
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3200
3817
|
if (command === "context" && hasFlag("--help")) {
|
|
3201
3818
|
contextHelp();
|
|
3202
3819
|
return;
|
|
@@ -3233,10 +3850,7 @@ async function main() {
|
|
|
3233
3850
|
return;
|
|
3234
3851
|
}
|
|
3235
3852
|
|
|
3236
|
-
if (
|
|
3237
|
-
command === "backend" &&
|
|
3238
|
-
(subcommand === "help" || hasFlag("--help"))
|
|
3239
|
-
) {
|
|
3853
|
+
if (command === "backend" && (subcommand === "help" || hasFlag("--help"))) {
|
|
3240
3854
|
backendHelp();
|
|
3241
3855
|
return;
|
|
3242
3856
|
}
|
|
@@ -3258,23 +3872,43 @@ async function main() {
|
|
|
3258
3872
|
const { runCrmCommand } = await import("./crm");
|
|
3259
3873
|
const runtime = await getRuntime();
|
|
3260
3874
|
// CRM is org-scoped — accept either an agent session token
|
|
3261
|
-
// (`dch_agent_*` from `dench
|
|
3875
|
+
// (`dch_agent_*` from `dench signin`) or a unified Dench API key
|
|
3262
3876
|
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
3263
3877
|
// `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
|
|
3264
3878
|
// bearer prefix and resolves the org for both.
|
|
3265
3879
|
if (runtime.mode === "dev") {
|
|
3880
|
+
const gatewayAuth =
|
|
3881
|
+
subArgs[0] === "enrich"
|
|
3882
|
+
? await resolveGatewayCommandAuth(runtime, "dench crm enrich")
|
|
3883
|
+
: undefined;
|
|
3266
3884
|
await runCrmCommand({
|
|
3267
3885
|
convex: runtime.client,
|
|
3268
3886
|
args: subArgs,
|
|
3269
3887
|
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
3888
|
+
enrichmentGateway: gatewayAuth
|
|
3889
|
+
? {
|
|
3890
|
+
apiKey: gatewayAuth.bearerToken,
|
|
3891
|
+
baseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3892
|
+
}
|
|
3893
|
+
: undefined,
|
|
3270
3894
|
});
|
|
3271
3895
|
return;
|
|
3272
3896
|
}
|
|
3273
3897
|
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench crm");
|
|
3898
|
+
const gatewayAuth =
|
|
3899
|
+
subArgs[0] === "enrich"
|
|
3900
|
+
? await resolveGatewayCommandAuth(authedRuntime, "dench crm enrich")
|
|
3901
|
+
: undefined;
|
|
3274
3902
|
await runCrmCommand({
|
|
3275
3903
|
convex: authedRuntime.client,
|
|
3276
3904
|
args: subArgs,
|
|
3277
3905
|
sessionToken: authedRuntime.sessionToken,
|
|
3906
|
+
enrichmentGateway: gatewayAuth
|
|
3907
|
+
? {
|
|
3908
|
+
apiKey: gatewayAuth.bearerToken,
|
|
3909
|
+
baseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3910
|
+
}
|
|
3911
|
+
: undefined,
|
|
3278
3912
|
});
|
|
3279
3913
|
return;
|
|
3280
3914
|
}
|
|
@@ -3307,10 +3941,7 @@ async function main() {
|
|
|
3307
3941
|
});
|
|
3308
3942
|
return;
|
|
3309
3943
|
}
|
|
3310
|
-
const authedRuntime = requireAuthenticatedRuntime(
|
|
3311
|
-
runtime,
|
|
3312
|
-
"dench members",
|
|
3313
|
-
);
|
|
3944
|
+
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench members");
|
|
3314
3945
|
await runMembersCommand({
|
|
3315
3946
|
convex: authedRuntime.client,
|
|
3316
3947
|
args: subArgs,
|
|
@@ -3335,44 +3966,76 @@ async function main() {
|
|
|
3335
3966
|
}
|
|
3336
3967
|
|
|
3337
3968
|
if (command === "search") {
|
|
3338
|
-
// `dench search` talks straight to the gateway over HTTP using
|
|
3339
|
-
// DENCH_API_KEY; it deliberately bypasses requireSessionRuntime so
|
|
3340
|
-
// it works in any sandbox / CI environment that has the key.
|
|
3341
3969
|
const subArgs = args.slice(args.indexOf("search") + 1);
|
|
3342
3970
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
3343
3971
|
const { runSearchCommand } = await import("./search");
|
|
3344
|
-
|
|
3972
|
+
const sub = filteredSubArgs[0];
|
|
3973
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
3974
|
+
await runSearchCommand({ args: filteredSubArgs, jsonOutput: json });
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
const runtime = await getRuntime();
|
|
3978
|
+
const gatewayAuth = await resolveGatewayCommandAuth(
|
|
3979
|
+
runtime,
|
|
3980
|
+
"dench search",
|
|
3981
|
+
);
|
|
3982
|
+
await runSearchCommand({
|
|
3983
|
+
args: filteredSubArgs,
|
|
3984
|
+
jsonOutput: json,
|
|
3985
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
3986
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3987
|
+
});
|
|
3345
3988
|
return;
|
|
3346
3989
|
}
|
|
3347
3990
|
|
|
3348
3991
|
if (command === "image") {
|
|
3349
|
-
// `dench image generate|edit` mirrors `dench search`: straight HTTP
|
|
3350
|
-
// to the gateway with DENCH_API_KEY, no Convex session required.
|
|
3351
|
-
// The gateway writes the bytes to Convex Storage on success; we
|
|
3352
|
-
// also drop them on local disk so the next bash command sees them.
|
|
3353
3992
|
const subArgs = args.slice(args.indexOf("image") + 1);
|
|
3354
3993
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
3355
3994
|
const { runImageCommand } = await import("./image");
|
|
3356
|
-
|
|
3995
|
+
const sub = filteredSubArgs[0];
|
|
3996
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
3997
|
+
await runImageCommand({ args: filteredSubArgs, jsonOutput: json });
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
const runtime = await getRuntime();
|
|
4001
|
+
const gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench image");
|
|
4002
|
+
await runImageCommand({
|
|
4003
|
+
args: filteredSubArgs,
|
|
4004
|
+
jsonOutput: json,
|
|
4005
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
4006
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
4007
|
+
});
|
|
3357
4008
|
return;
|
|
3358
4009
|
}
|
|
3359
4010
|
|
|
3360
4011
|
if (command === "apps") {
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
4012
|
+
const runtime = await getRuntime();
|
|
4013
|
+
const gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench apps");
|
|
4014
|
+
await runToolCommand({
|
|
4015
|
+
args: ["status"],
|
|
4016
|
+
jsonOutput: json,
|
|
4017
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
4018
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
4019
|
+
});
|
|
3365
4020
|
return;
|
|
3366
4021
|
}
|
|
3367
4022
|
|
|
3368
4023
|
if (command === "tool") {
|
|
3369
|
-
// `dench tool {status,connect,search,run,disconnect}` is the model-facing
|
|
3370
|
-
// composio surface and only needs DENCH_API_KEY (Bearer) on the
|
|
3371
|
-
// gateway. Bypasses requireSessionRuntime entirely so it works in
|
|
3372
|
-
// every sandbox / API-key context.
|
|
3373
4024
|
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("tool") + 1));
|
|
3374
4025
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
3375
|
-
|
|
4026
|
+
const sub = filteredSubArgs[0];
|
|
4027
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
4028
|
+
await runToolCommand({ args: filteredSubArgs, jsonOutput: json });
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
const runtime = await getRuntime();
|
|
4032
|
+
const gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench tool");
|
|
4033
|
+
await runToolCommand({
|
|
4034
|
+
args: filteredSubArgs,
|
|
4035
|
+
jsonOutput: json,
|
|
4036
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
4037
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
4038
|
+
});
|
|
3376
4039
|
return;
|
|
3377
4040
|
}
|
|
3378
4041
|
|
|
@@ -3485,7 +4148,7 @@ async function main() {
|
|
|
3485
4148
|
const { runCronCommand } = await import("./cron");
|
|
3486
4149
|
const runtime = await getRuntime();
|
|
3487
4150
|
// Cron CRUD is org-scoped — accept either an agent session token
|
|
3488
|
-
// (`dch_agent_*` from `dench
|
|
4151
|
+
// (`dch_agent_*` from `dench signin`) or a unified Dench API key
|
|
3489
4152
|
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
3490
4153
|
// `requireCronAccess` (convex/lib/cronAccess.ts) dispatches on the
|
|
3491
4154
|
// bearer prefix and resolves the org for both. The Bearer token
|
|
@@ -3508,7 +4171,7 @@ async function main() {
|
|
|
3508
4171
|
// Dispatch all `dench identity / organisation / user / tools / mem /
|
|
3509
4172
|
// heartbeat / bootstrap / model / daily` traffic through the shared
|
|
3510
4173
|
// agent-config CLI module. Each command requires the same auth shape
|
|
3511
|
-
// as `dench cron` (Bearer DENCH_API_KEY or `dench
|
|
4174
|
+
// as `dench cron` (Bearer DENCH_API_KEY or `dench signin` session
|
|
3512
4175
|
// token) so server-side `requireCronAccess` accepts both.
|
|
3513
4176
|
const AGENT_CONFIG_COMMANDS = new Set([
|
|
3514
4177
|
"identity",
|
|
@@ -3582,17 +4245,8 @@ async function main() {
|
|
|
3582
4245
|
}
|
|
3583
4246
|
}
|
|
3584
4247
|
|
|
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();
|
|
4248
|
+
if (command === "signin") {
|
|
4249
|
+
await signin();
|
|
3596
4250
|
return;
|
|
3597
4251
|
}
|
|
3598
4252
|
|
|
@@ -3631,6 +4285,11 @@ async function main() {
|
|
|
3631
4285
|
return;
|
|
3632
4286
|
}
|
|
3633
4287
|
|
|
4288
|
+
if (command === "upgrade") {
|
|
4289
|
+
await denchUpgrade(runtime);
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
3634
4293
|
if (command === "context") {
|
|
3635
4294
|
const context = await buildContext(runtime);
|
|
3636
4295
|
print(json ? context : formatContext(context));
|
|
@@ -3828,24 +4487,6 @@ async function main() {
|
|
|
3828
4487
|
return;
|
|
3829
4488
|
}
|
|
3830
4489
|
|
|
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
4490
|
if (command === "project" && subcommand === "create") {
|
|
3850
4491
|
if (runtime.mode !== "dev") {
|
|
3851
4492
|
throw new Error(
|