@dench.com/cli 0.3.6 → 0.3.8
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 +5 -2
- package/agent.ts +95 -13
- package/chat-spawn.ts +65 -4
- package/chat.ts +134 -1
- package/crm.ts +188 -87
- package/dench.ts +118 -17
- package/lib/cli-args.ts +61 -4
- package/package.json +1 -1
package/dench.ts
CHANGED
|
@@ -327,6 +327,41 @@ function throwIfCliError(value: unknown) {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
function normalizeCliError(error: unknown): CliError {
|
|
331
|
+
if (error instanceof CliError) return error;
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
333
|
+
if (message.includes("Invalid dev agent key")) {
|
|
334
|
+
return new CliError("Invalid dev agent key", {
|
|
335
|
+
code: "invalid_dev_agent_key",
|
|
336
|
+
nextActions: [
|
|
337
|
+
"Check that DENCH_DEV_AGENT_KEY in your shell matches the Convex deployment env.",
|
|
338
|
+
"Use --dev only for a local/dev Convex deployment configured with that key.",
|
|
339
|
+
"For normal terminal usage, run dench login or set DENCH_API_KEY + CONVEX_URL.",
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (message.includes("Dev CRM access is disabled in production")) {
|
|
344
|
+
return new CliError("Dev CRM access is disabled in production", {
|
|
345
|
+
code: "dev_access_disabled",
|
|
346
|
+
nextActions: [
|
|
347
|
+
"Remove --dev and run dench login for terminal access.",
|
|
348
|
+
"Or use DENCH_API_KEY + CONVEX_URL for sandbox/automation access.",
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (message.includes("Invalid Dench API key")) {
|
|
353
|
+
return new CliError("Invalid Dench API key", {
|
|
354
|
+
code: "invalid_api_key",
|
|
355
|
+
nextActions: [
|
|
356
|
+
"Check DENCH_API_KEY and CONVEX_URL point to the same organization/deployment.",
|
|
357
|
+
"Unset DENCH_API_KEY to use a saved dench login session instead.",
|
|
358
|
+
"Or run dench login to create a new terminal session.",
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return new CliError(message);
|
|
363
|
+
}
|
|
364
|
+
|
|
330
365
|
function parseJsonObjectOption(name: string) {
|
|
331
366
|
const raw = option(name);
|
|
332
367
|
if (!raw) return {};
|
|
@@ -461,8 +496,12 @@ Past chat threads (own + shared in this workspace):
|
|
|
461
496
|
Spawn a new chat-turn workflow (same as the web UI):
|
|
462
497
|
dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
463
498
|
dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
464
|
-
dench
|
|
465
|
-
dench
|
|
499
|
+
dench chat follow <thread-id> [--json] (tail a live thread by id)
|
|
500
|
+
dench chat tree [<root-thread-id>] [--limit N] [--json]
|
|
501
|
+
(parent-child thread tree)
|
|
502
|
+
dench agent new ... (alias for dench chat new)
|
|
503
|
+
dench agent send ... (alias for dench chat send)
|
|
504
|
+
dench agent follow ... (alias for dench chat follow)
|
|
466
505
|
|
|
467
506
|
Help: dench chat help
|
|
468
507
|
|
|
@@ -1755,6 +1794,30 @@ async function getRuntime() {
|
|
|
1755
1794
|
});
|
|
1756
1795
|
}
|
|
1757
1796
|
|
|
1797
|
+
// Sandbox session-mode path: workflow minting provisions a real
|
|
1798
|
+
// dch_agent_* token at sandbox-create time. Prefer it over apiKey so
|
|
1799
|
+
// agentWorkspace.* commands (Composio tools) work inside sandboxes.
|
|
1800
|
+
const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
|
|
1801
|
+
const sandboxConvexUrl = resolveApiKeyConvexUrl();
|
|
1802
|
+
if (sandboxAgentToken && sandboxConvexUrl) {
|
|
1803
|
+
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
1804
|
+
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
1805
|
+
const apiHost =
|
|
1806
|
+
process.env.DENCH_API_URL?.trim() ||
|
|
1807
|
+
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1808
|
+
host;
|
|
1809
|
+
return {
|
|
1810
|
+
mode: "session" as const,
|
|
1811
|
+
host: apiHost,
|
|
1812
|
+
client: new ConvexHttpClient(sandboxConvexUrl),
|
|
1813
|
+
sessionToken: sandboxAgentToken,
|
|
1814
|
+
organization:
|
|
1815
|
+
orgId && orgSlug
|
|
1816
|
+
? { id: orgId, name: orgSlug, slug: orgSlug }
|
|
1817
|
+
: undefined,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1758
1821
|
// Sandbox / automation path: the unified Dench API key + Convex URL
|
|
1759
1822
|
// baked into sandbox envVars (see
|
|
1760
1823
|
// src/lib/secrets/sandbox-tokens.ts) is enough to drive every
|
|
@@ -1781,10 +1844,12 @@ async function getRuntime() {
|
|
|
1781
1844
|
: undefined,
|
|
1782
1845
|
};
|
|
1783
1846
|
}
|
|
1847
|
+
|
|
1848
|
+
throw loginRequiredError();
|
|
1784
1849
|
}
|
|
1785
1850
|
|
|
1786
1851
|
if (!hasDevEnv()) {
|
|
1787
|
-
throw
|
|
1852
|
+
throw missingDevEnvError();
|
|
1788
1853
|
}
|
|
1789
1854
|
|
|
1790
1855
|
return {
|
|
@@ -1799,6 +1864,22 @@ async function getRuntime() {
|
|
|
1799
1864
|
|
|
1800
1865
|
type Runtime = Awaited<ReturnType<typeof getRuntime>>;
|
|
1801
1866
|
|
|
1867
|
+
function stripRuntimeFlags(args: string[]) {
|
|
1868
|
+
const booleanFlags = new Set(["--dev", "--prod", "--staging"]);
|
|
1869
|
+
const valueFlags = new Set(["--convex-url", "--host"]);
|
|
1870
|
+
const stripped: string[] = [];
|
|
1871
|
+
for (let i = 0; i < args.length; i++) {
|
|
1872
|
+
const arg = args[i];
|
|
1873
|
+
if (booleanFlags.has(arg)) continue;
|
|
1874
|
+
if (valueFlags.has(arg)) {
|
|
1875
|
+
i++;
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
stripped.push(arg);
|
|
1879
|
+
}
|
|
1880
|
+
return stripped;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1802
1883
|
/**
|
|
1803
1884
|
* `session` mode is required for `agentWorkspace.*` calls because they
|
|
1804
1885
|
* gate on `validateAgentSession` server-side, which only accepts the
|
|
@@ -1837,6 +1918,13 @@ function requireAuthenticatedRuntime(
|
|
|
1837
1918
|
throw loginRequiredError(command);
|
|
1838
1919
|
}
|
|
1839
1920
|
|
|
1921
|
+
function encodeDevCrmSessionToken(args: {
|
|
1922
|
+
workspaceSlug: string;
|
|
1923
|
+
devKey: string;
|
|
1924
|
+
}) {
|
|
1925
|
+
return `dch_dev_${encodeURIComponent(args.workspaceSlug)}:${encodeURIComponent(args.devKey)}`;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1840
1928
|
function agentSessionRequiredError(command: string) {
|
|
1841
1929
|
return new CliError(
|
|
1842
1930
|
`${command} requires an agent session — DENCH_API_KEY is org-scoped and cannot impersonate a specific agent`,
|
|
@@ -3458,7 +3546,7 @@ async function main() {
|
|
|
3458
3546
|
}
|
|
3459
3547
|
|
|
3460
3548
|
if (command === "crm") {
|
|
3461
|
-
const subArgs = args.slice(args.indexOf("crm") + 1);
|
|
3549
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("crm") + 1));
|
|
3462
3550
|
const sub = subArgs[0];
|
|
3463
3551
|
// Short-circuit help so users don't need a session to read the
|
|
3464
3552
|
// surface. The crm dispatcher's own help resolves --json + the
|
|
@@ -3478,6 +3566,14 @@ async function main() {
|
|
|
3478
3566
|
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
3479
3567
|
// `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
|
|
3480
3568
|
// bearer prefix and resolves the org for both.
|
|
3569
|
+
if (runtime.mode === "dev") {
|
|
3570
|
+
await runCrmCommand({
|
|
3571
|
+
convex: runtime.client,
|
|
3572
|
+
args: subArgs,
|
|
3573
|
+
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
3574
|
+
});
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3481
3577
|
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench crm");
|
|
3482
3578
|
await runCrmCommand({
|
|
3483
3579
|
convex: authedRuntime.client,
|
|
@@ -3534,12 +3630,13 @@ async function main() {
|
|
|
3534
3630
|
await runChatCommand({ convex: {} as any, args: subArgs });
|
|
3535
3631
|
return;
|
|
3536
3632
|
}
|
|
3537
|
-
// `chat new` / `chat send`
|
|
3538
|
-
// /api/chat/cli
|
|
3539
|
-
//
|
|
3540
|
-
//
|
|
3541
|
-
// queries and keep going
|
|
3542
|
-
|
|
3633
|
+
// `chat new` / `chat send` / `chat follow` use HTTP routes
|
|
3634
|
+
// (/api/chat/cli for spawn, /api/chat/[runId]/stream and
|
|
3635
|
+
// /api/chat/stream for follow) so they need an authenticated host
|
|
3636
|
+
// + bearer token, not a Convex client. The remaining subcommands
|
|
3637
|
+
// (list / search / read / tree) are Convex queries and keep going
|
|
3638
|
+
// through `runChatCommand`.
|
|
3639
|
+
if (sub === "new" || sub === "send" || sub === "follow") {
|
|
3543
3640
|
const { runChatSpawnCommand } = await import("./chat-spawn");
|
|
3544
3641
|
const runtime = await getRuntime();
|
|
3545
3642
|
const authedRuntime = requireAuthenticatedRuntime(
|
|
@@ -3583,11 +3680,12 @@ async function main() {
|
|
|
3583
3680
|
await runAgentCommand({ convex: {} as any, args: subArgs });
|
|
3584
3681
|
return;
|
|
3585
3682
|
}
|
|
3586
|
-
// Aliases: `dench agent new` / `dench agent send`
|
|
3587
|
-
// implementation. Surfaced under
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3590
|
-
|
|
3683
|
+
// Aliases: `dench agent new` / `dench agent send` / `dench agent
|
|
3684
|
+
// follow` -> the chat-spawn implementation. Surfaced under
|
|
3685
|
+
// `agent` so users who associate chat threads with the agent
|
|
3686
|
+
// surface still find them; the actual workflow is identical to
|
|
3687
|
+
// `dench chat new` / `dench chat send` / `dench chat follow`.
|
|
3688
|
+
if (sub === "new" || sub === "send" || sub === "follow") {
|
|
3591
3689
|
const { runChatSpawnCommand } = await import("./chat-spawn");
|
|
3592
3690
|
const runtime = await getRuntime();
|
|
3593
3691
|
const authedRuntime = requireAuthenticatedRuntime(
|
|
@@ -3609,6 +3707,8 @@ async function main() {
|
|
|
3609
3707
|
await runAgentCommand({
|
|
3610
3708
|
convex: runtime.client,
|
|
3611
3709
|
args: subArgs,
|
|
3710
|
+
sessionToken: runtime.sessionToken,
|
|
3711
|
+
callerRunId: process.env.DENCH_RUN_ID?.trim() || undefined,
|
|
3612
3712
|
});
|
|
3613
3713
|
return;
|
|
3614
3714
|
}
|
|
@@ -4237,11 +4337,12 @@ async function main() {
|
|
|
4237
4337
|
}
|
|
4238
4338
|
|
|
4239
4339
|
main().catch((error) => {
|
|
4240
|
-
const
|
|
4340
|
+
const normalized = normalizeCliError(error);
|
|
4341
|
+
const extra = normalized.payload;
|
|
4241
4342
|
const payload: JsonRecord = {
|
|
4242
4343
|
ok: false,
|
|
4243
4344
|
...extra,
|
|
4244
|
-
error:
|
|
4345
|
+
error: normalized.message,
|
|
4245
4346
|
};
|
|
4246
4347
|
if (json) {
|
|
4247
4348
|
console.error(JSON.stringify(payload, null, 2));
|
package/lib/cli-args.ts
CHANGED
|
@@ -7,9 +7,20 @@
|
|
|
7
7
|
* - shift(args, expected): pop the next positional arg or throw.
|
|
8
8
|
* - getFlag(args, name): pull the value following `--name` and remove
|
|
9
9
|
* both tokens from args. Returns undefined if the flag is missing.
|
|
10
|
+
* - getFlagWithAliases(args, primary, aliases): like getFlag but
|
|
11
|
+
* accepts multiple flag names; the first one found wins. Used to
|
|
12
|
+
* accept e.g. `--data` and `--fields` interchangeably so a confused
|
|
13
|
+
* caller doesn't silently produce blank rows.
|
|
10
14
|
* - hasFlag(args, name): true iff `--name` appears (and removes it).
|
|
11
15
|
* - parseJson(value): JSON.parse with a CliError-style throw on
|
|
12
16
|
* failure.
|
|
17
|
+
* - assertNoUnknownFlags(args, command): after all known flags have
|
|
18
|
+
* been drained via getFlag/hasFlag, throw if any token starting
|
|
19
|
+
* with `--` is left in args. Catches typos like `--fields` when
|
|
20
|
+
* the subcommand only knows `--data` — the previous behaviour
|
|
21
|
+
* silently dropped the typo and produced empty writes (see
|
|
22
|
+
* incident in chat y5710jn4313ht2p6fwmw0m0mjn8636ya where 15 blank
|
|
23
|
+
* CRM rows were created before the agent realized).
|
|
13
24
|
*
|
|
14
25
|
* NOTE: each module wraps its own *CliError class so we don't import
|
|
15
26
|
* across cli/ entry points; this module's errors throw plain Error and
|
|
@@ -24,10 +35,7 @@ export function shift(args: string[], expected: string): string {
|
|
|
24
35
|
return value;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
export function getFlag(
|
|
28
|
-
args: string[],
|
|
29
|
-
name: string,
|
|
30
|
-
): string | undefined {
|
|
38
|
+
export function getFlag(args: string[], name: string): string | undefined {
|
|
31
39
|
const idx = args.indexOf(name);
|
|
32
40
|
if (idx === -1) return undefined;
|
|
33
41
|
const value = args[idx + 1];
|
|
@@ -35,6 +43,36 @@ export function getFlag(
|
|
|
35
43
|
return value;
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
export function getFlagWithAliases(
|
|
47
|
+
args: string[],
|
|
48
|
+
primary: string,
|
|
49
|
+
aliases: readonly string[],
|
|
50
|
+
): string | undefined {
|
|
51
|
+
// Try each name in order; the first one found is consumed and returned.
|
|
52
|
+
// Aliases (and duplicate occurrences of the picked name) are still
|
|
53
|
+
// removed from args so unknown-flag detection downstream doesn't flag
|
|
54
|
+
// them as typos and so the caller can't get bitten by silent
|
|
55
|
+
// first-wins drift across multiple invocations.
|
|
56
|
+
const all = [primary, ...aliases];
|
|
57
|
+
let chosen: string | undefined;
|
|
58
|
+
for (const name of all) {
|
|
59
|
+
const value = getFlag(args, name);
|
|
60
|
+
if (value !== undefined) {
|
|
61
|
+
chosen = value;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Drain any remaining occurrences of every alias (including the
|
|
66
|
+
// primary). With a single getFlag pass per name we'd leave a
|
|
67
|
+
// duplicate `--fields` behind to look like an unknown flag.
|
|
68
|
+
for (const name of all) {
|
|
69
|
+
while (getFlag(args, name) !== undefined) {
|
|
70
|
+
// Intentional empty body: drain duplicates.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return chosen;
|
|
74
|
+
}
|
|
75
|
+
|
|
38
76
|
export function hasFlag(args: string[], name: string): boolean {
|
|
39
77
|
const idx = args.indexOf(name);
|
|
40
78
|
if (idx === -1) return false;
|
|
@@ -50,3 +88,22 @@ export function parseJson(value: string | undefined): unknown {
|
|
|
50
88
|
throw new CliArgError(`Invalid JSON: ${value}`);
|
|
51
89
|
}
|
|
52
90
|
}
|
|
91
|
+
|
|
92
|
+
export function assertNoUnknownFlags(
|
|
93
|
+
args: readonly string[],
|
|
94
|
+
command: string,
|
|
95
|
+
): void {
|
|
96
|
+
// After every getFlag/hasFlag/getFlagWithAliases call has had a chance
|
|
97
|
+
// to drain known flags, any remaining token that starts with `--` is
|
|
98
|
+
// unknown. We surface the first one with a hint so the caller can fix
|
|
99
|
+
// the typo. Don't include positional `--` literals; raw `--` (used by
|
|
100
|
+
// some shells to terminate option parsing) is allowed.
|
|
101
|
+
for (const token of args) {
|
|
102
|
+
if (token === "--") continue;
|
|
103
|
+
if (token.startsWith("--")) {
|
|
104
|
+
throw new CliArgError(
|
|
105
|
+
`Unknown flag for ${command}: ${token}. Run \`dench ${command.split(" ")[0]} help\` for the full flag list.`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|