@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/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 agent new ... (alias for dench chat new)
465
- dench agent send ... (alias for dench chat send)
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 hasFlag("--dev") ? missingDevEnvError() : loginRequiredError();
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` spawn a chat-turn workflow via
3538
- // /api/chat/cli (the same workflow the web UI uses). They need
3539
- // an HTTP host + bearer token, not a Convex client. The
3540
- // remaining subcommands (list / search / read) are Convex
3541
- // queries and keep going through `runChatCommand`.
3542
- if (sub === "new" || sub === "send") {
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` -> the chat-spawn
3587
- // implementation. Surfaced under `agent` so users who associate
3588
- // chat threads with the agent surface still find them; the actual
3589
- // workflow is identical to `dench chat new` / `dench chat send`.
3590
- if (sub === "new" || sub === "send") {
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 extra = error instanceof CliError ? error.payload : {};
4340
+ const normalized = normalizeCliError(error);
4341
+ const extra = normalized.payload;
4241
4342
  const payload: JsonRecord = {
4242
4343
  ok: false,
4243
4344
  ...extra,
4244
- error: error instanceof Error ? error.message : String(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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {