@dench.com/cli 0.4.5 → 0.4.7
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 +40 -2
- package/agent.ts +17 -453
- package/crm.ts +4 -51
- package/cron.ts +4 -9
- package/dench.ts +324 -295
- package/fs-daemon.ts +26 -18
- package/host.ts +60 -1
- package/lib/enrichment-gateway.ts +7 -4
- package/lib/load-dev-env.mjs +90 -15
- package/lib/load-dev-env.test.mjs +44 -0
- package/package.json +1 -1
- package/tools.ts +6 -2
package/dench.ts
CHANGED
|
@@ -18,7 +18,12 @@ import { agentKindLabel, normalizeAgentKind } from "./agentKind";
|
|
|
18
18
|
import {
|
|
19
19
|
DEFAULT_HOST,
|
|
20
20
|
isLocalHost,
|
|
21
|
+
LOCAL_HOST,
|
|
21
22
|
normalizeHost,
|
|
23
|
+
PRODUCTION_API_URL,
|
|
24
|
+
PRODUCTION_CONVEX_URL,
|
|
25
|
+
PRODUCTION_HOST,
|
|
26
|
+
resolveBackendAlias,
|
|
22
27
|
resolveHostFromArgs,
|
|
23
28
|
STAGING_HOST,
|
|
24
29
|
} from "./host";
|
|
@@ -29,7 +34,6 @@ import {
|
|
|
29
34
|
listStoredSessionEntries,
|
|
30
35
|
removeAllStoredSessions,
|
|
31
36
|
removeStoredSession,
|
|
32
|
-
resolveConfigHost,
|
|
33
37
|
resolveSessionScope,
|
|
34
38
|
type SessionScope,
|
|
35
39
|
type StoredSessionEntry,
|
|
@@ -65,6 +69,24 @@ type StoredSession = {
|
|
|
65
69
|
};
|
|
66
70
|
|
|
67
71
|
type ConfigFile = {
|
|
72
|
+
/**
|
|
73
|
+
* Explicit user choice of which Dench backend to talk to by default,
|
|
74
|
+
* set via `dench backend set <local|staging|prod|<url>>`. Takes
|
|
75
|
+
* precedence over the legacy `currentHost` field (which used to be
|
|
76
|
+
* silently updated by every `dench login` and caused the CLI to
|
|
77
|
+
* silently follow the last-logged-in environment).
|
|
78
|
+
*
|
|
79
|
+
* Resolution order in `resolveHost`:
|
|
80
|
+
* 1. CLI flags (`--prod`, `--staging`, `--backend`, `--host`)
|
|
81
|
+
* 2. `DENCH_HOST` env var
|
|
82
|
+
* 3. `defaultBackend` from this config
|
|
83
|
+
* 4. `PRODUCTION_HOST` (the safe fallback)
|
|
84
|
+
*
|
|
85
|
+
* `currentHost` / `currentHosts` / `currentSessionKey` /
|
|
86
|
+
* `currentSessionKeys` keep their old role of remembering which
|
|
87
|
+
* stored session to use, but they no longer drive backend choice.
|
|
88
|
+
*/
|
|
89
|
+
defaultBackend?: string;
|
|
68
90
|
currentHost?: string;
|
|
69
91
|
currentSessionKey?: string;
|
|
70
92
|
currentHosts?: Record<string, string>;
|
|
@@ -197,16 +219,6 @@ const api = {
|
|
|
197
219
|
};
|
|
198
220
|
const CONFIG_DIR = join(homedir(), ".dench");
|
|
199
221
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
200
|
-
const AUTONOMOUS_DEFAULT_API_BASE = "http://localhost:3000";
|
|
201
|
-
const SAFE_LONG_SESSION_CONSTRAINTS = [
|
|
202
|
-
"do not edit files",
|
|
203
|
-
"publish",
|
|
204
|
-
"deploy",
|
|
205
|
-
"spend money",
|
|
206
|
-
"send external messages",
|
|
207
|
-
"access secrets",
|
|
208
|
-
"or change production data unless the user explicitly approves it",
|
|
209
|
-
];
|
|
210
222
|
const args = process.argv.slice(2);
|
|
211
223
|
const json = args.includes("--json");
|
|
212
224
|
const filteredArgs = args.filter((arg) => arg !== "--json");
|
|
@@ -254,6 +266,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
254
266
|
"--staging",
|
|
255
267
|
"--prod",
|
|
256
268
|
"--no-open",
|
|
269
|
+
"--use-dev-env",
|
|
257
270
|
"--version",
|
|
258
271
|
"--yolo",
|
|
259
272
|
"--ask-before-publishing",
|
|
@@ -417,6 +430,7 @@ Usage:
|
|
|
417
430
|
dench sessions [--host <host>] [--json]
|
|
418
431
|
dench use <session-key-or-workspace-slug> [--host <host>] [--json]
|
|
419
432
|
dench logout [session-key-or-workspace-slug] [--session <key-or-scope>] [--host <host>] [--all] [--json]
|
|
433
|
+
dench backend [show|set|clear] [<production|staging|local|<url>>] [--json]
|
|
420
434
|
dench context [--json]
|
|
421
435
|
dench status [--mine|--self] [--json]
|
|
422
436
|
dench tasks [--json]
|
|
@@ -442,8 +456,6 @@ Usage:
|
|
|
442
456
|
dench tool connect <toolkit> [--json]
|
|
443
457
|
dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
|
|
444
458
|
dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connected_account_id>] [--approval <approvalId>] [--json]
|
|
445
|
-
dench autonomous run "Inspect this repo and summarize the relevant feature. Do not edit files, publish, deploy, spend money, send external messages, access secrets, or change production data." [--duration 1h] [--model <model>] [--api-base http://localhost:3000] [--yolo] [--json]
|
|
446
|
-
|
|
447
459
|
CRM (Daytona-native rewrite):
|
|
448
460
|
dench crm objects <list|get|create|update|rename|delete> ...
|
|
449
461
|
dench crm fields <list|create|update|delete|reorder> <object> ...
|
|
@@ -561,13 +573,25 @@ Agent setup:
|
|
|
561
573
|
Use dench memory and dench artifacts to continue from durable context.
|
|
562
574
|
Do not create, claim, or log a setup task by default.
|
|
563
575
|
|
|
564
|
-
|
|
565
|
-
Default
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
576
|
+
Backends and hosts:
|
|
577
|
+
Default backend: ${DEFAULT_HOST} (overridden by \`dench backend set\`)
|
|
578
|
+
Show / change the persistent default:
|
|
579
|
+
dench backend show
|
|
580
|
+
dench backend set production | staging | local | <url>
|
|
581
|
+
dench backend clear
|
|
582
|
+
One-shot overrides for a single command:
|
|
583
|
+
dench --prod <cmd>
|
|
584
|
+
dench --staging <cmd>
|
|
585
|
+
dench --backend local <cmd>
|
|
586
|
+
dench --host https://example.com <cmd>
|
|
587
|
+
DENCH_HOST=https://example.com dench <cmd>
|
|
588
|
+
|
|
589
|
+
Dev fallback (run against a local Convex deployment with a dev key):
|
|
570
590
|
DENCH_DEV_AGENT_KEY=... DENCH_WORKSPACE=... NEXT_PUBLIC_CONVEX_URL=... dench status --dev
|
|
591
|
+
Inside the dench.com repo, --dev also auto-loads .env / .env.local
|
|
592
|
+
so GATEWAY_URL etc. flow through. Use --use-dev-env (or
|
|
593
|
+
DENCH_USE_DEV_ENV=1) when you want that auto-load without switching
|
|
594
|
+
the runtime to the dev workspace mode.
|
|
571
595
|
|
|
572
596
|
Advanced:
|
|
573
597
|
For long-lived agents or when the human asks:
|
|
@@ -602,26 +626,6 @@ Notes:
|
|
|
602
626
|
`);
|
|
603
627
|
}
|
|
604
628
|
|
|
605
|
-
function autonomousHelp() {
|
|
606
|
-
console.log(`Dench Long Session commands
|
|
607
|
-
|
|
608
|
-
Usage:
|
|
609
|
-
dench autonomous run "Inspect this repo and summarize the relevant feature. Do not edit files, publish, deploy, spend money, send external messages, access secrets, or change production data." [--duration 1h] [--model <model>] [--api-base http://localhost:3000] [--yolo] [--json]
|
|
610
|
-
|
|
611
|
-
Notes:
|
|
612
|
-
Starts a Long Session through /api/runs.
|
|
613
|
-
Duration supports 30m, 1h, 3h, 5h, or until-done.
|
|
614
|
-
API base defaults to ${AUTONOMOUS_DEFAULT_API_BASE}.
|
|
615
|
-
Override with --api-base, --host, DENCH_AUTONOMOUS_API_BASE, or DENCH_HOST.
|
|
616
|
-
If needed, run dench login --host <api-base> once so the CLI can authenticate.
|
|
617
|
-
Omit --model to use the server default.
|
|
618
|
-
Approval flags: --ask-before-publishing --ask-before-spending --ask-before-external-messages --ask-before-production-changes --ask-before-secrets.
|
|
619
|
-
--yolo: skip every approval gate (overrides --ask-before-* / --approval-rules). Equivalent to the workspace's YOLO mode in /<slug>/settings -> Approvals.
|
|
620
|
-
Without any approval flags, the run inherits the workspace's Approvals policy from /<slug>/settings.
|
|
621
|
-
Safe goals should explicitly say: ${SAFE_LONG_SESSION_CONSTRAINTS.join(", ")}.
|
|
622
|
-
`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
629
|
function contextHelp() {
|
|
626
630
|
console.log(`Dench context
|
|
627
631
|
|
|
@@ -653,8 +657,8 @@ Usage:
|
|
|
653
657
|
dench suggested-work [--json]
|
|
654
658
|
dench suggested-work task <artifactId> [--json]
|
|
655
659
|
|
|
656
|
-
Artifacts are durable
|
|
657
|
-
|
|
660
|
+
Artifacts are durable outputs for human or agent review. Suggested work can
|
|
661
|
+
be converted into a workspace task.
|
|
658
662
|
`);
|
|
659
663
|
}
|
|
660
664
|
|
|
@@ -667,8 +671,8 @@ Usage:
|
|
|
667
671
|
dench what-can-i-do [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
|
|
668
672
|
|
|
669
673
|
Logs in if needed, then shows what this agent can do here: workspace rules,
|
|
670
|
-
open work, suggested work,
|
|
671
|
-
|
|
674
|
+
open work, suggested work, connected-tool commands, memory, approvals, and
|
|
675
|
+
next actions.
|
|
672
676
|
`);
|
|
673
677
|
}
|
|
674
678
|
|
|
@@ -760,8 +764,49 @@ Notes:
|
|
|
760
764
|
`);
|
|
761
765
|
}
|
|
762
766
|
|
|
763
|
-
function
|
|
764
|
-
|
|
767
|
+
function backendHelp() {
|
|
768
|
+
console.log(`Dench backend
|
|
769
|
+
|
|
770
|
+
Usage:
|
|
771
|
+
dench backend # alias for "show"
|
|
772
|
+
dench backend show [--json]
|
|
773
|
+
dench backend set <production|staging|local|<url>> [--json]
|
|
774
|
+
dench backend clear [--json]
|
|
775
|
+
|
|
776
|
+
Aliases: prod = production, dev / localhost = local, stage = staging.
|
|
777
|
+
|
|
778
|
+
What this does:
|
|
779
|
+
The default Dench backend is production (${PRODUCTION_HOST}). "dench backend set"
|
|
780
|
+
stores an explicit override in ~/.dench/config.json so every future
|
|
781
|
+
"dench" invocation talks to that backend by default. Use it instead of
|
|
782
|
+
relying on a stale currentHost from a past "dench login".
|
|
783
|
+
|
|
784
|
+
Setting the backend to "local" also opts the CLI into auto-loading
|
|
785
|
+
.env and .env.local from the dench.com workspace at startup, which is
|
|
786
|
+
needed for GATEWAY_URL and similar local-only env vars.
|
|
787
|
+
|
|
788
|
+
One-shot overrides (no config change):
|
|
789
|
+
dench --prod <cmd> # one command against production
|
|
790
|
+
dench --staging <cmd> # one command against staging
|
|
791
|
+
dench --backend local <cmd> # one command against http://localhost:3000
|
|
792
|
+
dench --host https://… <cmd> # one command against a custom URL
|
|
793
|
+
DENCH_HOST=https://… dench … # same, via env
|
|
794
|
+
|
|
795
|
+
Opt into dev .env auto-loading without changing the default backend:
|
|
796
|
+
--use-dev-env # one-shot flag
|
|
797
|
+
DENCH_USE_DEV_ENV=1 # env var (e.g. via direnv)
|
|
798
|
+
`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function resolveHost(config: ConfigFile | undefined, _scope: SessionScope) {
|
|
802
|
+
// `defaultBackend` is only ever set via `dench backend set`, so it
|
|
803
|
+
// represents an explicit user choice. Production wins on fresh
|
|
804
|
+
// installs (no flags, no env, no config) — which is the safe default
|
|
805
|
+
// for an agent CLI. The per-scope `currentHosts[scope.key]` map is
|
|
806
|
+
// intentionally NOT consulted here so that a past `dench login
|
|
807
|
+
// --host http://localhost:3000` doesn't silently keep routing every
|
|
808
|
+
// future command at local dev.
|
|
809
|
+
return resolveHostFromArgs(filteredArgs, config?.defaultBackend);
|
|
765
810
|
}
|
|
766
811
|
|
|
767
812
|
async function loadConfig(): Promise<ConfigFile> {
|
|
@@ -900,6 +945,194 @@ function formatSessionChoices(
|
|
|
900
945
|
return lines.length > 0 ? lines.join("\n") : " No Dench sessions found.";
|
|
901
946
|
}
|
|
902
947
|
|
|
948
|
+
function backendLabelForHost(host: string) {
|
|
949
|
+
const normalized = normalizeHost(host);
|
|
950
|
+
if (normalized === PRODUCTION_HOST) return "production";
|
|
951
|
+
if (normalized === STAGING_HOST) return "staging";
|
|
952
|
+
if (isLocalHost(normalized)) return "local";
|
|
953
|
+
return "custom";
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function backendSnapshot(config: ConfigFile) {
|
|
957
|
+
const resolved = resolveHost(config, {
|
|
958
|
+
key: "auto:backend-show",
|
|
959
|
+
label: "cwd",
|
|
960
|
+
});
|
|
961
|
+
const stored = config.defaultBackend?.trim()
|
|
962
|
+
? normalizeHost(config.defaultBackend.trim())
|
|
963
|
+
: null;
|
|
964
|
+
const envHost = process.env.DENCH_HOST?.trim() || null;
|
|
965
|
+
const argBackend =
|
|
966
|
+
option("--backend") ?? (hasFlag("--prod")
|
|
967
|
+
? "production"
|
|
968
|
+
: hasFlag("--staging")
|
|
969
|
+
? "staging"
|
|
970
|
+
: option("--host") ?? null);
|
|
971
|
+
|
|
972
|
+
let source: "flag" | "env" | "config" | "default";
|
|
973
|
+
if (argBackend) source = "flag";
|
|
974
|
+
else if (envHost) source = "env";
|
|
975
|
+
else if (stored) source = "config";
|
|
976
|
+
else source = "default";
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
host: resolved,
|
|
980
|
+
label: backendLabelForHost(resolved),
|
|
981
|
+
source,
|
|
982
|
+
defaultBackend: stored,
|
|
983
|
+
envHost,
|
|
984
|
+
knownBackends: {
|
|
985
|
+
production: PRODUCTION_HOST,
|
|
986
|
+
staging: STAGING_HOST,
|
|
987
|
+
local: LOCAL_HOST,
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function formatBackendShow(snapshot: ReturnType<typeof backendSnapshot>) {
|
|
993
|
+
const sourceHint =
|
|
994
|
+
snapshot.source === "flag"
|
|
995
|
+
? "(set via CLI flag for this invocation)"
|
|
996
|
+
: snapshot.source === "env"
|
|
997
|
+
? "(set via DENCH_HOST env var)"
|
|
998
|
+
: snapshot.source === "config"
|
|
999
|
+
? "(set via `dench backend set`)"
|
|
1000
|
+
: "(default — no explicit choice in env or config)";
|
|
1001
|
+
const lines = [
|
|
1002
|
+
`Active backend: ${snapshot.host} [${snapshot.label}] ${sourceHint}`,
|
|
1003
|
+
];
|
|
1004
|
+
if (snapshot.defaultBackend && snapshot.source !== "config") {
|
|
1005
|
+
lines.push(`Stored default backend: ${snapshot.defaultBackend}`);
|
|
1006
|
+
}
|
|
1007
|
+
if (snapshot.envHost) {
|
|
1008
|
+
lines.push(`DENCH_HOST env: ${snapshot.envHost}`);
|
|
1009
|
+
}
|
|
1010
|
+
lines.push(
|
|
1011
|
+
"",
|
|
1012
|
+
"Manage default backend:",
|
|
1013
|
+
" dench backend set production # default fallback",
|
|
1014
|
+
" dench backend set staging",
|
|
1015
|
+
" dench backend set local # http://localhost:3000",
|
|
1016
|
+
" dench backend set https://example.com # custom URL",
|
|
1017
|
+
" dench backend clear # revert to production default",
|
|
1018
|
+
"",
|
|
1019
|
+
"One-shot override flags also work on every command:",
|
|
1020
|
+
" dench --prod <cmd> dench --staging <cmd>",
|
|
1021
|
+
" dench --backend local <cmd> dench --host https://example.com <cmd>",
|
|
1022
|
+
);
|
|
1023
|
+
return lines.join("\n");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function backendShow() {
|
|
1027
|
+
const config = await loadConfig();
|
|
1028
|
+
const snapshot = backendSnapshot(config);
|
|
1029
|
+
print(json ? snapshot : formatBackendShow(snapshot));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function backendSet() {
|
|
1033
|
+
const target = positional(2);
|
|
1034
|
+
if (!target) {
|
|
1035
|
+
throw new CliError(
|
|
1036
|
+
"Missing backend value. Pass local, staging, prod, or a URL.",
|
|
1037
|
+
{
|
|
1038
|
+
code: "backend_set_missing_value",
|
|
1039
|
+
nextActions: [
|
|
1040
|
+
"Run dench backend set production.",
|
|
1041
|
+
"Run dench backend set local.",
|
|
1042
|
+
"Run dench backend set https://your-host.example.com.",
|
|
1043
|
+
],
|
|
1044
|
+
},
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
let resolved: string;
|
|
1048
|
+
try {
|
|
1049
|
+
resolved = resolveBackendAlias(target);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1052
|
+
throw new CliError(message, {
|
|
1053
|
+
code: "backend_set_invalid_value",
|
|
1054
|
+
nextActions: [
|
|
1055
|
+
"Run dench backend set production / staging / local.",
|
|
1056
|
+
"Or pass an explicit URL: dench backend set https://your-host.example.com.",
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
const config = await loadConfig();
|
|
1061
|
+
await saveConfig({ ...config, defaultBackend: resolved });
|
|
1062
|
+
const label = backendLabelForHost(resolved);
|
|
1063
|
+
if (json) {
|
|
1064
|
+
print({
|
|
1065
|
+
ok: true,
|
|
1066
|
+
defaultBackend: resolved,
|
|
1067
|
+
label,
|
|
1068
|
+
message: `Default Dench backend set to ${resolved} (${label}).`,
|
|
1069
|
+
});
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
print(`Default Dench backend set to ${resolved} (${label}).`);
|
|
1073
|
+
if (label === "local") {
|
|
1074
|
+
print(
|
|
1075
|
+
"Local backend selected. dench will now also load .env / .env.local from the workspace so GATEWAY_URL etc. are picked up.",
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
print("Future `dench` commands without --prod/--staging/--host will target this backend.");
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function backendClear() {
|
|
1082
|
+
const config = await loadConfig();
|
|
1083
|
+
if (!config.defaultBackend) {
|
|
1084
|
+
print(
|
|
1085
|
+
json
|
|
1086
|
+
? { ok: true, cleared: false, defaultBackend: null }
|
|
1087
|
+
: "No default backend is set. dench already falls back to production.",
|
|
1088
|
+
);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const previous = config.defaultBackend;
|
|
1092
|
+
const next: ConfigFile = { ...config };
|
|
1093
|
+
delete next.defaultBackend;
|
|
1094
|
+
await saveConfig(next);
|
|
1095
|
+
print(
|
|
1096
|
+
json
|
|
1097
|
+
? {
|
|
1098
|
+
ok: true,
|
|
1099
|
+
cleared: true,
|
|
1100
|
+
previous,
|
|
1101
|
+
defaultBackend: null,
|
|
1102
|
+
fallback: PRODUCTION_HOST,
|
|
1103
|
+
}
|
|
1104
|
+
: `Cleared default backend (was ${previous}). dench will fall back to ${PRODUCTION_HOST}.`,
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function runBackendCommand() {
|
|
1109
|
+
const sub = positional(1);
|
|
1110
|
+
if (!sub || sub === "show" || sub === "status") {
|
|
1111
|
+
await backendShow();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (sub === "set") {
|
|
1115
|
+
await backendSet();
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (sub === "clear" || sub === "reset" || sub === "unset") {
|
|
1119
|
+
await backendClear();
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
1123
|
+
backendHelp();
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
throw new CliError(`Unknown dench backend subcommand: ${sub}`, {
|
|
1127
|
+
code: "unknown_backend_subcommand",
|
|
1128
|
+
nextActions: [
|
|
1129
|
+
"Run dench backend show to print the active backend.",
|
|
1130
|
+
"Run dench backend set <local|staging|prod|<url>>.",
|
|
1131
|
+
"Run dench backend clear to revert to production.",
|
|
1132
|
+
],
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
903
1136
|
async function listSessions() {
|
|
904
1137
|
const config = await loadConfig();
|
|
905
1138
|
const scope = resolveSessionScope({ args: filteredArgs });
|
|
@@ -1395,176 +1628,6 @@ function normalizeApiBase(input: string) {
|
|
|
1395
1628
|
return new URL(withProtocol).origin;
|
|
1396
1629
|
}
|
|
1397
1630
|
|
|
1398
|
-
function resolveAutonomousApiBase() {
|
|
1399
|
-
const explicit =
|
|
1400
|
-
option("--api-base") ??
|
|
1401
|
-
option("--host") ??
|
|
1402
|
-
process.env.DENCH_AUTONOMOUS_API_BASE ??
|
|
1403
|
-
process.env.DENCH_HOST;
|
|
1404
|
-
return explicit ? normalizeApiBase(explicit) : AUTONOMOUS_DEFAULT_API_BASE;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
async function resolveAutonomousSessionToken(apiBase: string) {
|
|
1408
|
-
const config = await loadConfig();
|
|
1409
|
-
const scope = resolveSessionScope({ args: filteredArgs });
|
|
1410
|
-
const stored = await getStoredSession(apiBase, scope);
|
|
1411
|
-
if (stored.status === "found") {
|
|
1412
|
-
return stored.session.sessionToken;
|
|
1413
|
-
}
|
|
1414
|
-
if (stored.status === "ambiguous") {
|
|
1415
|
-
throw new Error(ambiguousSessionMessage(config, scope, apiBase));
|
|
1416
|
-
}
|
|
1417
|
-
return undefined;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
function formatAutonomousRunResult(value: unknown) {
|
|
1421
|
-
const record = asRecord(value);
|
|
1422
|
-
if (!record) {
|
|
1423
|
-
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
const runId = stringField(record, "runId", "id");
|
|
1427
|
-
const workflowRunId = stringField(record, "workflowRunId");
|
|
1428
|
-
const status = stringField(record, "status");
|
|
1429
|
-
const url = stringField(record, "url", "runUrl");
|
|
1430
|
-
|
|
1431
|
-
if (!runId && !workflowRunId && !status && !url) {
|
|
1432
|
-
return JSON.stringify(value, null, 2);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
const lines = ["Started Long Session"];
|
|
1436
|
-
if (runId) lines.push(`Run ID: ${runId}`);
|
|
1437
|
-
if (workflowRunId) lines.push(`Workflow Run ID: ${workflowRunId}`);
|
|
1438
|
-
if (status) lines.push(`Status: ${status}`);
|
|
1439
|
-
if (url) lines.push(`URL: ${url}`);
|
|
1440
|
-
return lines.join("\n");
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
function autonomousRunUrl(apiBase: string, runId: string) {
|
|
1444
|
-
return `${apiBase.replace(/\/+$/, "")}/labs/autonomous/${encodeURIComponent(runId)}`;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function withAutonomousRunUrl(payload: unknown, apiBase: string) {
|
|
1448
|
-
const record = asRecord(payload);
|
|
1449
|
-
const runId = stringField(record, "runId", "id");
|
|
1450
|
-
if (!record || !runId || stringField(record, "runUrl", "url")) {
|
|
1451
|
-
return payload;
|
|
1452
|
-
}
|
|
1453
|
-
return { ...record, runUrl: autonomousRunUrl(apiBase, runId) };
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
function formatAutonomousRunError(args: {
|
|
1457
|
-
status: number;
|
|
1458
|
-
payload: unknown;
|
|
1459
|
-
apiBase: string;
|
|
1460
|
-
hasSession: boolean;
|
|
1461
|
-
statusText: string;
|
|
1462
|
-
}) {
|
|
1463
|
-
const record = asRecord(args.payload);
|
|
1464
|
-
const error =
|
|
1465
|
-
stringField(record, "error", "message") ??
|
|
1466
|
-
(typeof args.payload === "string" && args.payload.trim()
|
|
1467
|
-
? args.payload.trim()
|
|
1468
|
-
: args.statusText);
|
|
1469
|
-
const nextActions = stringArrayField(record, "nextActions");
|
|
1470
|
-
if (args.status === 401 && !args.hasSession) {
|
|
1471
|
-
nextActions.unshift(`Run: dench login --host ${args.apiBase}`);
|
|
1472
|
-
} else if (args.status === 401) {
|
|
1473
|
-
nextActions.push(
|
|
1474
|
-
"Run dench sessions --json to inspect saved sessions.",
|
|
1475
|
-
`Run dench login --host ${args.apiBase} if this session is expired.`,
|
|
1476
|
-
);
|
|
1477
|
-
} else if (args.status === 402 && nextActions.length === 0) {
|
|
1478
|
-
nextActions.push(
|
|
1479
|
-
"Top up AI credits in Dench usage/billing.",
|
|
1480
|
-
"Retry the same Long Session command after credits update.",
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
if (stringField(record, "topUpUrl")) {
|
|
1484
|
-
nextActions.unshift(`Top up: ${stringField(record, "topUpUrl")}`);
|
|
1485
|
-
}
|
|
1486
|
-
return new CliError(`Long Session failed (${args.status}): ${error}`, {
|
|
1487
|
-
status: args.status,
|
|
1488
|
-
code: stringField(record, "code") ?? `http_${args.status}`,
|
|
1489
|
-
apiBase: args.apiBase,
|
|
1490
|
-
topUpUrl: stringField(record, "topUpUrl"),
|
|
1491
|
-
nextActions: [...new Set(nextActions)],
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
function safeLongSessionExample() {
|
|
1496
|
-
return 'dench autonomous run "Inspect this repo and summarize the relevant feature. Do not edit files, publish, deploy, spend money, send external messages, access secrets, or change production data." --duration 30m';
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
function parseDurationMs(rawDuration?: string) {
|
|
1500
|
-
const raw = rawDuration?.trim().toLowerCase();
|
|
1501
|
-
if (!raw) return undefined;
|
|
1502
|
-
if (["until-done", "until_done", "done", "max"].includes(raw)) {
|
|
1503
|
-
return 24 * 60 * 60 * 1000;
|
|
1504
|
-
}
|
|
1505
|
-
const match = raw.match(
|
|
1506
|
-
/^(\d+(?:\.\d+)?)(m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/,
|
|
1507
|
-
);
|
|
1508
|
-
if (!match) {
|
|
1509
|
-
throw new CliError(
|
|
1510
|
-
"--duration must look like 30m, 1h, 3h, 5h, or until-done",
|
|
1511
|
-
{
|
|
1512
|
-
code: "invalid_duration",
|
|
1513
|
-
nextActions: [
|
|
1514
|
-
"Use one of: 30m, 1h, 3h, 5h, until-done.",
|
|
1515
|
-
safeLongSessionExample(),
|
|
1516
|
-
],
|
|
1517
|
-
},
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
const value = Number(match[1]);
|
|
1521
|
-
const unit = match[2];
|
|
1522
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
1523
|
-
throw new CliError("--duration must be positive", {
|
|
1524
|
-
code: "invalid_duration",
|
|
1525
|
-
nextActions: ["Use a positive duration like 30m or 1h."],
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
const minutes = unit.startsWith("h") ? value * 60 : value;
|
|
1529
|
-
return Math.round(minutes * 60 * 1000);
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
function durationPreset(rawDuration?: string) {
|
|
1533
|
-
const raw = rawDuration?.trim().toLowerCase();
|
|
1534
|
-
if (!raw) return undefined;
|
|
1535
|
-
return raw.replace("-", "_");
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
function parseApprovalRules() {
|
|
1539
|
-
// --yolo wins over everything else: every approval gate is off,
|
|
1540
|
-
// mirroring the org-level YOLO mode (settings -> Approvals).
|
|
1541
|
-
if (hasFlag("--yolo")) return [];
|
|
1542
|
-
const explicit = option("--approval-rules")
|
|
1543
|
-
?.split(",")
|
|
1544
|
-
.map((rule) => rule.trim())
|
|
1545
|
-
.filter(Boolean);
|
|
1546
|
-
if (explicit && explicit.length > 0) return explicit;
|
|
1547
|
-
const rules: string[] = [];
|
|
1548
|
-
if (hasFlag("--ask-before-publishing")) rules.push("publishing");
|
|
1549
|
-
if (hasFlag("--ask-before-spending")) rules.push("spending");
|
|
1550
|
-
if (hasFlag("--ask-before-external-messages")) {
|
|
1551
|
-
rules.push("external_messages");
|
|
1552
|
-
}
|
|
1553
|
-
if (hasFlag("--ask-before-production-changes")) {
|
|
1554
|
-
rules.push("production_changes");
|
|
1555
|
-
}
|
|
1556
|
-
if (hasFlag("--ask-before-secrets")) rules.push("secrets");
|
|
1557
|
-
return rules.length > 0
|
|
1558
|
-
? rules
|
|
1559
|
-
: [
|
|
1560
|
-
"publishing",
|
|
1561
|
-
"spending",
|
|
1562
|
-
"external_messages",
|
|
1563
|
-
"production_changes",
|
|
1564
|
-
"secrets",
|
|
1565
|
-
];
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
1631
|
async function parseResponseBody(response: Response) {
|
|
1569
1632
|
const text = await response.text();
|
|
1570
1633
|
if (!text) return {};
|
|
@@ -1575,65 +1638,6 @@ async function parseResponseBody(response: Response) {
|
|
|
1575
1638
|
}
|
|
1576
1639
|
}
|
|
1577
1640
|
|
|
1578
|
-
async function startAutonomousRun() {
|
|
1579
|
-
const goal = positionalsFrom(2).join(" ").trim();
|
|
1580
|
-
if (!goal) {
|
|
1581
|
-
throw new CliError("Missing goal", {
|
|
1582
|
-
code: "missing_goal",
|
|
1583
|
-
nextActions: [
|
|
1584
|
-
safeLongSessionExample(),
|
|
1585
|
-
"Keep the goal specific and include safety constraints for risky actions.",
|
|
1586
|
-
],
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
const model = option("--model")?.trim();
|
|
1591
|
-
const duration = option("--duration");
|
|
1592
|
-
const body: JsonRecord = {
|
|
1593
|
-
goal,
|
|
1594
|
-
metadata: {
|
|
1595
|
-
sessionKind: "long_session",
|
|
1596
|
-
approvalRules: parseApprovalRules(),
|
|
1597
|
-
safetyConstraints: SAFE_LONG_SESSION_CONSTRAINTS,
|
|
1598
|
-
...(duration ? { durationPreset: durationPreset(duration) } : {}),
|
|
1599
|
-
},
|
|
1600
|
-
};
|
|
1601
|
-
if (model) body.model = model;
|
|
1602
|
-
const timeBudgetMs = parseDurationMs(duration);
|
|
1603
|
-
if (timeBudgetMs) body.timeBudgetMs = timeBudgetMs;
|
|
1604
|
-
|
|
1605
|
-
const apiBase = resolveAutonomousApiBase();
|
|
1606
|
-
const agentSessionToken = await resolveAutonomousSessionToken(apiBase);
|
|
1607
|
-
const headers: Record<string, string> = {
|
|
1608
|
-
accept: "application/json",
|
|
1609
|
-
"content-type": "application/json",
|
|
1610
|
-
};
|
|
1611
|
-
if (agentSessionToken) {
|
|
1612
|
-
headers.authorization = `Bearer ${agentSessionToken}`;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
const response = await fetch(`${apiBase}/api/runs`, {
|
|
1616
|
-
method: "POST",
|
|
1617
|
-
headers,
|
|
1618
|
-
body: JSON.stringify(body),
|
|
1619
|
-
});
|
|
1620
|
-
const payload = await parseResponseBody(response);
|
|
1621
|
-
|
|
1622
|
-
if (!response.ok) {
|
|
1623
|
-
throw formatAutonomousRunError({
|
|
1624
|
-
status: response.status,
|
|
1625
|
-
payload,
|
|
1626
|
-
apiBase,
|
|
1627
|
-
hasSession: Boolean(agentSessionToken),
|
|
1628
|
-
statusText: response.statusText,
|
|
1629
|
-
});
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
const output = withAutonomousRunUrl(payload, apiBase);
|
|
1633
|
-
throwIfCliError(output);
|
|
1634
|
-
print(json ? output : formatAutonomousRunResult(output));
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
1641
|
function formatUsd(cents: number | undefined) {
|
|
1638
1642
|
const value = typeof cents === "number" && Number.isFinite(cents) ? cents : 0;
|
|
1639
1643
|
return `$${(value / 100).toFixed(2)}`;
|
|
@@ -1748,12 +1752,37 @@ function hasDevEnv() {
|
|
|
1748
1752
|
* dev typically only has `NEXT_PUBLIC_CONVEX_URL` exported. We accept
|
|
1749
1753
|
* either so the same code path works in both shells without callers
|
|
1750
1754
|
* needing to know which env var is set.
|
|
1755
|
+
*
|
|
1756
|
+
* When neither env var is set we fall back to the baked-in production
|
|
1757
|
+
* Convex URL (`PRODUCTION_CONVEX_URL`). This is a deliberate
|
|
1758
|
+
* defense-in-depth move: if a sandbox is provisioned while a Vercel
|
|
1759
|
+
* deployment is half-configured and `CONVEX_URL` lands as empty
|
|
1760
|
+
* string, the CLI inside it still ends up pointed at the production
|
|
1761
|
+
* deployment instead of crashing on a "missing Convex URL" error.
|
|
1762
|
+
* Dev / staging shells override this by exporting `CONVEX_URL` or
|
|
1763
|
+
* `NEXT_PUBLIC_CONVEX_URL` explicitly.
|
|
1751
1764
|
*/
|
|
1752
1765
|
function resolveApiKeyConvexUrl(): string | undefined {
|
|
1753
1766
|
return (
|
|
1754
1767
|
process.env.CONVEX_URL?.trim() ||
|
|
1755
1768
|
process.env.NEXT_PUBLIC_CONVEX_URL?.trim() ||
|
|
1756
|
-
|
|
1769
|
+
PRODUCTION_CONVEX_URL
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Resolve the Next.js API host the CLI should hit for HTTP calls (e.g.
|
|
1775
|
+
* Stripe billing topup, the gateway proxy). Same defense-in-depth
|
|
1776
|
+
* pattern as `resolveApiKeyConvexUrl`: env wins, then the resolved
|
|
1777
|
+
* Dench host the runtime is targeting, and finally
|
|
1778
|
+
* `PRODUCTION_API_URL` as the last resort.
|
|
1779
|
+
*/
|
|
1780
|
+
function resolveApiHost(fallbackHost: string): string {
|
|
1781
|
+
return (
|
|
1782
|
+
process.env.DENCH_API_URL?.trim() ||
|
|
1783
|
+
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1784
|
+
fallbackHost ||
|
|
1785
|
+
PRODUCTION_API_URL
|
|
1757
1786
|
);
|
|
1758
1787
|
}
|
|
1759
1788
|
|
|
@@ -1817,10 +1846,7 @@ async function getRuntime() {
|
|
|
1817
1846
|
if (sandboxAgentToken && sandboxConvexUrl) {
|
|
1818
1847
|
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
1819
1848
|
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
1820
|
-
const apiHost =
|
|
1821
|
-
process.env.DENCH_API_URL?.trim() ||
|
|
1822
|
-
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1823
|
-
host;
|
|
1849
|
+
const apiHost = resolveApiHost(host);
|
|
1824
1850
|
return {
|
|
1825
1851
|
mode: "session" as const,
|
|
1826
1852
|
host: apiHost,
|
|
@@ -1844,10 +1870,7 @@ async function getRuntime() {
|
|
|
1844
1870
|
if (apiKey && convexUrl) {
|
|
1845
1871
|
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
1846
1872
|
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
1847
|
-
const apiHost =
|
|
1848
|
-
process.env.DENCH_API_URL?.trim() ||
|
|
1849
|
-
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1850
|
-
host;
|
|
1873
|
+
const apiHost = resolveApiHost(host);
|
|
1851
1874
|
return {
|
|
1852
1875
|
mode: "apiKey" as const,
|
|
1853
1876
|
host: apiHost,
|
|
@@ -1880,8 +1903,13 @@ async function getRuntime() {
|
|
|
1880
1903
|
type Runtime = Awaited<ReturnType<typeof getRuntime>>;
|
|
1881
1904
|
|
|
1882
1905
|
function stripRuntimeFlags(args: string[]) {
|
|
1883
|
-
const booleanFlags = new Set([
|
|
1884
|
-
|
|
1906
|
+
const booleanFlags = new Set([
|
|
1907
|
+
"--dev",
|
|
1908
|
+
"--prod",
|
|
1909
|
+
"--staging",
|
|
1910
|
+
"--use-dev-env",
|
|
1911
|
+
]);
|
|
1912
|
+
const valueFlags = new Set(["--convex-url", "--host", "--backend"]);
|
|
1885
1913
|
const stripped: string[] = [];
|
|
1886
1914
|
for (let i = 0; i < args.length; i++) {
|
|
1887
1915
|
const arg = args[i];
|
|
@@ -3198,16 +3226,12 @@ async function main() {
|
|
|
3198
3226
|
return;
|
|
3199
3227
|
}
|
|
3200
3228
|
|
|
3201
|
-
if (
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
await startAutonomousRun();
|
|
3208
|
-
return;
|
|
3209
|
-
}
|
|
3210
|
-
throw new Error(`Unknown autonomous command: ${filteredArgs.join(" ")}`);
|
|
3229
|
+
if (
|
|
3230
|
+
command === "backend" &&
|
|
3231
|
+
(subcommand === "help" || hasFlag("--help"))
|
|
3232
|
+
) {
|
|
3233
|
+
backendHelp();
|
|
3234
|
+
return;
|
|
3211
3235
|
}
|
|
3212
3236
|
|
|
3213
3237
|
if (command === "crm") {
|
|
@@ -3540,6 +3564,11 @@ async function main() {
|
|
|
3540
3564
|
return;
|
|
3541
3565
|
}
|
|
3542
3566
|
|
|
3567
|
+
if (command === "backend") {
|
|
3568
|
+
await runBackendCommand();
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3543
3572
|
const runtime = await getRuntime();
|
|
3544
3573
|
|
|
3545
3574
|
if (command === "billing") {
|