@dench.com/cli 0.3.1 → 0.3.3

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
@@ -179,6 +179,21 @@ const api = {
179
179
  "functions/agentToolsNode:agentToolStatus",
180
180
  ),
181
181
  },
182
+ files: {
183
+ listTree: makeFunctionReference<"query">("functions/files:listTree"),
184
+ deleteFile: makeFunctionReference<"mutation">(
185
+ "functions/files:deleteFile",
186
+ ),
187
+ generateUploadUrl: makeFunctionReference<"mutation">(
188
+ "functions/files:generateUploadUrl",
189
+ ),
190
+ commitFileUpload: makeFunctionReference<"mutation">(
191
+ "functions/files:commitFileUpload",
192
+ ),
193
+ getFileSignedDownloadUrl: makeFunctionReference<"action">(
194
+ "functions/files:getFileSignedDownloadUrl",
195
+ ),
196
+ },
182
197
  },
183
198
  };
184
199
  const CONFIG_DIR = join(homedir(), ".dench");
@@ -233,6 +248,7 @@ const BOOLEAN_FLAGS = new Set([
233
248
  "--all",
234
249
  "--compact",
235
250
  "--dev",
251
+ "--follow",
236
252
  "--help",
237
253
  "--mine",
238
254
  "--self",
@@ -240,6 +256,7 @@ const BOOLEAN_FLAGS = new Set([
240
256
  "--prod",
241
257
  "--no-open",
242
258
  "--version",
259
+ "--yolo",
243
260
  "--ask-before-publishing",
244
261
  "--ask-before-spending",
245
262
  "--ask-before-external-messages",
@@ -391,7 +408,7 @@ Usage:
391
408
  dench tool connect <toolkit> [--json]
392
409
  dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
393
410
  dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connected_account_id>] [--approval <approvalId>] [--json]
394
- 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] [--json]
411
+ 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]
395
412
 
396
413
  CRM (Daytona-native rewrite):
397
414
  dench crm objects <list|get|create|update|rename|delete> ...
@@ -413,12 +430,32 @@ CRM (Daytona-native rewrite):
413
430
  dench crm batch --file ops.jsonl
414
431
  Help: dench crm help
415
432
 
433
+ Past chat threads (own + shared in this workspace):
434
+ dench chat list [--query <substr>] [--limit N] [--include-archived] [--json]
435
+ dench chat search "<query>" [--limit N] [--include-current] [--json]
436
+ dench chat read <thread-id> [--limit N] [--json]
437
+
438
+ Spawn a new chat-turn workflow (same as the web UI):
439
+ dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
440
+ dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
441
+ dench agent new ... (alias for dench chat new)
442
+ dench agent send ... (alias for dench chat send)
443
+
444
+ Help: dench chat help
445
+
416
446
  File sync (Daytona-native rewrite):
447
+ dench fs status [--json] [--workspace /workspace] [--no-hash] [--drift-limit N]
417
448
  dench fs sync --initial --org-id <orgId> [--workspace /workspace]
418
449
  dench fs stream-open <path>
419
450
  dench fs stream-append <token> <chunk>
420
451
  dench fs stream-close <token>
421
452
 
453
+ Workspace files (use these instead of mv/rm in /workspace; rename(2)
454
+ returns ENOSYS on the FUSE-backed volume mount):
455
+ dench files ls <path> [--json]
456
+ dench files mv <src> <dst> [--json]
457
+ dench files rm <path> [--recursive] [--json]
458
+
422
459
  Subagents (Daytona-native rewrite):
423
460
  dench agent spawn '<goal>' [--parent-run-id <id>] [--sandbox own|share_parent] [--time-budget-ms N]
424
461
  dench agent await --hook <token> | --children id1,id2,id3
@@ -491,7 +528,7 @@ function autonomousHelp() {
491
528
  console.log(`Dench Long Session commands
492
529
 
493
530
  Usage:
494
- 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] [--json]
531
+ 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]
495
532
 
496
533
  Notes:
497
534
  Starts a Long Session through /api/runs.
@@ -501,6 +538,8 @@ Notes:
501
538
  If needed, run dench login --host <api-base> once so the CLI can authenticate.
502
539
  Omit --model to use the server default.
503
540
  Approval flags: --ask-before-publishing --ask-before-spending --ask-before-external-messages --ask-before-production-changes --ask-before-secrets.
541
+ --yolo: skip every approval gate (overrides --ask-before-* / --approval-rules). Equivalent to the workspace's YOLO mode in /<slug>/settings -> Approvals.
542
+ Without any approval flags, the run inherits the workspace's Approvals policy from /<slug>/settings.
504
543
  Safe goals should explicitly say: ${SAFE_LONG_SESSION_CONSTRAINTS.join(", ")}.
505
544
  `);
506
545
  }
@@ -1417,6 +1456,9 @@ function durationPreset(rawDuration?: string) {
1417
1456
  }
1418
1457
 
1419
1458
  function parseApprovalRules() {
1459
+ // --yolo wins over everything else: every approval gate is off,
1460
+ // mirroring the org-level YOLO mode (settings -> Approvals).
1461
+ if (hasFlag("--yolo")) return [];
1420
1462
  const explicit = option("--approval-rules")
1421
1463
  ?.split(",")
1422
1464
  .map((rule) => rule.trim())
@@ -1620,11 +1662,27 @@ function hasDevEnv() {
1620
1662
  );
1621
1663
  }
1622
1664
 
1665
+ /**
1666
+ * Resolve the Convex URL the API-key runtime should talk to. Sandboxes
1667
+ * bake `CONVEX_URL` (see `src/lib/secrets/sandbox-tokens.ts`); local
1668
+ * dev typically only has `NEXT_PUBLIC_CONVEX_URL` exported. We accept
1669
+ * either so the same code path works in both shells without callers
1670
+ * needing to know which env var is set.
1671
+ */
1672
+ function resolveApiKeyConvexUrl(): string | undefined {
1673
+ return (
1674
+ process.env.CONVEX_URL?.trim() ||
1675
+ process.env.NEXT_PUBLIC_CONVEX_URL?.trim() ||
1676
+ undefined
1677
+ );
1678
+ }
1679
+
1623
1680
  function loginRequiredError(command = "this command") {
1624
1681
  return new CliError(`${command} requires dench login`, {
1625
1682
  code: "login_required",
1626
1683
  nextActions: [
1627
- 'Run dench onboard --kind <kind> --name "AI Agent - Project".',
1684
+ "Set DENCH_API_KEY (and CONVEX_URL) in your env if running inside a sandbox or CI.",
1685
+ 'Or run dench onboard --kind <kind> --name "AI Agent - Project" to mint an agent session.',
1628
1686
  "If already approved, run dench sessions --json, then dench use <session-key-or-workspace-slug>.",
1629
1687
  ],
1630
1688
  });
@@ -1670,6 +1728,33 @@ async function getRuntime() {
1670
1728
  ],
1671
1729
  });
1672
1730
  }
1731
+
1732
+ // Sandbox / automation path: the unified Dench API key + Convex URL
1733
+ // baked into sandbox envVars (see
1734
+ // src/lib/secrets/sandbox-tokens.ts) is enough to drive every
1735
+ // command that goes through `requireCrmAccess` server-side. We only
1736
+ // fall into this branch when no stored session is present, so
1737
+ // explicit `dench login` setups keep their existing behavior.
1738
+ const apiKey = process.env.DENCH_API_KEY?.trim();
1739
+ const convexUrl = resolveApiKeyConvexUrl();
1740
+ if (apiKey && convexUrl) {
1741
+ const orgId = process.env.DENCH_ORG_ID?.trim();
1742
+ const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
1743
+ const apiHost =
1744
+ process.env.DENCH_API_URL?.trim() ||
1745
+ process.env.DENCH_INTERNAL_BASE?.trim() ||
1746
+ host;
1747
+ return {
1748
+ mode: "apiKey" as const,
1749
+ host: apiHost,
1750
+ client: new ConvexHttpClient(convexUrl),
1751
+ sessionToken: apiKey,
1752
+ organization:
1753
+ orgId && orgSlug
1754
+ ? { id: orgId, name: orgSlug, slug: orgSlug }
1755
+ : undefined,
1756
+ };
1757
+ }
1673
1758
  }
1674
1759
 
1675
1760
  if (!hasDevEnv()) {
@@ -1688,14 +1773,73 @@ async function getRuntime() {
1688
1773
 
1689
1774
  type Runtime = Awaited<ReturnType<typeof getRuntime>>;
1690
1775
 
1776
+ /**
1777
+ * `session` mode is required for `agentWorkspace.*` calls because they
1778
+ * gate on `validateAgentSession` server-side, which only accepts the
1779
+ * `dch_agent_*` token format. Sandboxes that only have a unified
1780
+ * `DENCH_API_KEY` (apiKey mode) cannot impersonate a specific agent
1781
+ * and will fail at the server with `Invalid agent session` — surface
1782
+ * that here as a clear error instead.
1783
+ */
1691
1784
  function requireSessionRuntime(
1692
1785
  runtime: Runtime,
1693
1786
  command: string,
1694
1787
  ): Extract<Runtime, { mode: "session" }> {
1695
- if (runtime.mode !== "session") {
1696
- throw loginRequiredError(command);
1788
+ if (runtime.mode === "session") return runtime;
1789
+ if (runtime.mode === "apiKey") {
1790
+ throw agentSessionRequiredError(command);
1791
+ }
1792
+ throw loginRequiredError(command);
1793
+ }
1794
+
1795
+ /**
1796
+ * Org-scoped commands (`dench crm`, `dench files`, etc.) accept either
1797
+ * a `dch_agent_*` agent session token OR a unified Dench API key
1798
+ * (`dench_*` / `gw_sk_*` / etc.) baked into sandbox envVars. The
1799
+ * server-side `requireCrmAccess` helper dispatches on the prefix.
1800
+ */
1801
+ type AuthenticatedRuntime = Extract<
1802
+ Runtime,
1803
+ { mode: "session" | "apiKey" }
1804
+ >;
1805
+
1806
+ function requireAuthenticatedRuntime(
1807
+ runtime: Runtime,
1808
+ command: string,
1809
+ ): AuthenticatedRuntime {
1810
+ if (runtime.mode === "session" || runtime.mode === "apiKey") return runtime;
1811
+ throw loginRequiredError(command);
1812
+ }
1813
+
1814
+ function agentSessionRequiredError(command: string) {
1815
+ return new CliError(
1816
+ `${command} requires an agent session — DENCH_API_KEY is org-scoped and cannot impersonate a specific agent`,
1817
+ {
1818
+ code: "agent_session_required",
1819
+ nextActions: [
1820
+ 'Run dench onboard --kind <kind> --name "AI Agent - Project" to mint an agent session for this command.',
1821
+ "Or run the equivalent action via the workspace UI / dench crm if it is org-level (e.g. CRM data, files).",
1822
+ ],
1823
+ },
1824
+ );
1825
+ }
1826
+
1827
+ /**
1828
+ * Some legacy `agentWorkspace.*` commands (task create / claim / log /
1829
+ * approval request) have separate session vs dev branches. With apiKey
1830
+ * mode added, the dev branch would silently receive partial args. Use
1831
+ * this to short-circuit those branches with a clear error.
1832
+ *
1833
+ * Asserts away the `apiKey` mode so the caller's downstream type
1834
+ * narrowing (e.g. `runtime.workspaceArgs`) keeps working.
1835
+ */
1836
+ function rejectApiKeyForAgentCommand(
1837
+ runtime: Runtime,
1838
+ command: string,
1839
+ ): asserts runtime is Exclude<Runtime, { mode: "apiKey" }> {
1840
+ if (runtime.mode === "apiKey") {
1841
+ throw agentSessionRequiredError(command);
1697
1842
  }
1698
- return runtime;
1699
1843
  }
1700
1844
 
1701
1845
  async function printToolStatus(runtime: Runtime, toolkit?: string) {
@@ -1927,23 +2071,36 @@ async function connectedAppsContext(runtime: Runtime) {
1927
2071
  }
1928
2072
 
1929
2073
  async function buildContext(runtime: Runtime) {
1930
- const mine =
1931
- runtime.mode === "session"
1932
- ? ((await runtime.client.query(
1933
- api.functions.agentWorkspace.whatCanIDoHere,
1934
- {
1935
- sessionToken: runtime.sessionToken,
1936
- },
1937
- )) as JsonRecord)
1938
- : summarizeDevMine(
1939
- runtime.workspaceArgs.workspaceSlug,
1940
- await devOverview(runtime),
1941
- );
1942
-
1943
- const urls =
1944
- runtime.mode === "session"
1945
- ? workspaceUrls(runtime.host, workspaceSlugFromContext(mine))
2074
+ let mine: JsonRecord;
2075
+ let urls: JsonRecord;
2076
+ if (runtime.mode === "session") {
2077
+ mine = (await runtime.client.query(
2078
+ api.functions.agentWorkspace.whatCanIDoHere,
2079
+ { sessionToken: runtime.sessionToken },
2080
+ )) as JsonRecord;
2081
+ urls = workspaceUrls(runtime.host, workspaceSlugFromContext(mine));
2082
+ } else if (runtime.mode === "dev") {
2083
+ mine = summarizeDevMine(
2084
+ runtime.workspaceArgs.workspaceSlug,
2085
+ await devOverview(runtime),
2086
+ );
2087
+ urls = {};
2088
+ } else {
2089
+ // apiKey mode: no agent session is available, so we can't call
2090
+ // `whatCanIDoHere` (it gates on `validateAgentSession`). Return a
2091
+ // minimal context derived from sandbox envVars so dench-cli help
2092
+ // surfaces still work without a full login.
2093
+ mine = {
2094
+ authMode: "apiKey",
2095
+ organization: runtime.organization ?? null,
2096
+ runId: process.env.DENCH_RUN_ID?.trim() ?? null,
2097
+ parentRunId: process.env.DENCH_PARENT_RUN_ID?.trim() ?? null,
2098
+ rootRunId: process.env.DENCH_ROOT_RUN_ID?.trim() ?? null,
2099
+ };
2100
+ urls = runtime.host
2101
+ ? workspaceUrls(runtime.host, runtime.organization?.slug)
1946
2102
  : {};
2103
+ }
1947
2104
 
1948
2105
  return {
1949
2106
  ...mine,
@@ -2341,7 +2498,7 @@ function withToolRunGuidance(runtime: Runtime, result: unknown) {
2341
2498
  if (!record || record.requiresApproval !== true) return result;
2342
2499
  const approvalId = stringField(record, "approvalId");
2343
2500
  const statusContext =
2344
- runtime.mode === "session"
2501
+ runtime.mode === "session" || runtime.mode === "apiKey"
2345
2502
  ? workspaceUrls(runtime.host, runtime.organization?.slug)
2346
2503
  : {};
2347
2504
  const approvalCommand = approvalId
@@ -2501,6 +2658,12 @@ async function listArtifacts(runtime: Runtime, limit?: number) {
2501
2658
  );
2502
2659
  }
2503
2660
  }
2661
+ if (runtime.mode === "apiKey") {
2662
+ // No agent identity → no artifact list. Return empty so callers
2663
+ // (e.g. `dench artifacts`, `dench suggested-work`) degrade gracefully
2664
+ // instead of throwing for sandbox callers.
2665
+ return [] as JsonRecord[];
2666
+ }
2504
2667
  const data = await devOverview(runtime);
2505
2668
  return asRecordArray(data.recentArtifacts);
2506
2669
  }
@@ -2542,6 +2705,354 @@ async function runFsCommand() {
2542
2705
  }
2543
2706
  }
2544
2707
 
2708
+ // ── dench files <ls|mv|rm> ────────────────────────────────────────────────
2709
+ //
2710
+ // Escape hatches the model needs every time something on /workspace
2711
+ // can't be done with `mv` / `rm` — Mountpoint-S3 doesn't implement
2712
+ // rename(2) and the file daemon needs a hint to prune Convex rows.
2713
+ // All three commands take + emit POSIX paths under /workspace.
2714
+
2715
+ function normalizeFilesPath(input: string): string {
2716
+ if (!input || input === "/") return "/";
2717
+ let cleaned = input.replace(/\\/g, "/").replace(/\/+/g, "/");
2718
+ if (cleaned.startsWith("/workspace/")) cleaned = cleaned.slice("/workspace".length);
2719
+ else if (cleaned === "/workspace") cleaned = "/";
2720
+ if (!cleaned.startsWith("/")) cleaned = `/${cleaned}`;
2721
+ if (cleaned.length > 1 && cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
2722
+ return cleaned;
2723
+ }
2724
+
2725
+ function filesApiKey(runtime: Runtime): string | undefined {
2726
+ if (runtime.mode === "apiKey") return runtime.sessionToken;
2727
+ return process.env.DENCH_API_KEY?.trim() || undefined;
2728
+ }
2729
+
2730
+ function requireFilesApiKey(runtime: Runtime, command: string): string {
2731
+ const apiKey = filesApiKey(runtime);
2732
+ if (!apiKey) {
2733
+ throw new CliError(
2734
+ `${command} needs DENCH_API_KEY (or a sandbox-baked api-key session). It bypasses the user-session auth path.`,
2735
+ {
2736
+ code: "files_api_key_required",
2737
+ nextActions: [
2738
+ "Run inside a Dench sandbox where DENCH_API_KEY is baked in.",
2739
+ "Or export DENCH_API_KEY=<key> first (see dench login output).",
2740
+ ],
2741
+ },
2742
+ );
2743
+ }
2744
+ return apiKey;
2745
+ }
2746
+
2747
+ type FileTreeRow = {
2748
+ path: string;
2749
+ contentHash?: string;
2750
+ isDir: boolean;
2751
+ size?: number;
2752
+ mtime?: number;
2753
+ };
2754
+
2755
+ async function listConvexTreeAt(
2756
+ runtime: Runtime,
2757
+ apiKey: string,
2758
+ prefix: string,
2759
+ ): Promise<FileTreeRow[]> {
2760
+ return (await runtime.client.query(api.functions.files.listTree, {
2761
+ prefix,
2762
+ apiKey,
2763
+ } as never)) as FileTreeRow[];
2764
+ }
2765
+
2766
+ async function listConvexTreeRecursive(
2767
+ runtime: Runtime,
2768
+ apiKey: string,
2769
+ prefix: string,
2770
+ ): Promise<FileTreeRow[]> {
2771
+ const rows = await listConvexTreeAt(runtime, apiKey, prefix);
2772
+ const out: FileTreeRow[] = [...rows];
2773
+ for (const row of rows) {
2774
+ if (row.isDir) {
2775
+ const children = await listConvexTreeRecursive(runtime, apiKey, row.path);
2776
+ out.push(...children);
2777
+ }
2778
+ }
2779
+ return out;
2780
+ }
2781
+
2782
+ async function downloadConvexFileBytes(
2783
+ runtime: Runtime,
2784
+ apiKey: string,
2785
+ path: string,
2786
+ ): Promise<Uint8Array | null> {
2787
+ const url = (await runtime.client.action(
2788
+ api.functions.files.getFileSignedDownloadUrl,
2789
+ { path, apiKey } as never,
2790
+ )) as string | null;
2791
+ if (!url) return null;
2792
+ const response = await fetch(url);
2793
+ if (!response.ok) {
2794
+ throw new Error(
2795
+ `signed download failed (${response.status}): ${await response
2796
+ .text()
2797
+ .catch(() => "")}`,
2798
+ );
2799
+ }
2800
+ return new Uint8Array(await response.arrayBuffer());
2801
+ }
2802
+
2803
+ async function uploadConvexFileBytes(
2804
+ runtime: Runtime,
2805
+ apiKey: string,
2806
+ args: { path: string; bytes: Uint8Array },
2807
+ ): Promise<{ path: string; storageId: string; contentHash: string }> {
2808
+ const uploadUrl = (await runtime.client.mutation(
2809
+ api.functions.files.generateUploadUrl,
2810
+ { path: args.path, apiKey } as never,
2811
+ )) as string;
2812
+ const response = await fetch(uploadUrl, {
2813
+ method: "POST",
2814
+ headers: { "content-type": "application/octet-stream" },
2815
+ body: new Blob([args.bytes as BlobPart]),
2816
+ });
2817
+ if (!response.ok) {
2818
+ throw new Error(
2819
+ `convex storage upload failed (${response.status}): ${await response
2820
+ .text()
2821
+ .catch(() => "")}`,
2822
+ );
2823
+ }
2824
+ const { storageId } = (await response.json()) as { storageId: string };
2825
+ const hash = sha256HexSync(args.bytes);
2826
+ const inlineEligible = args.bytes.byteLength <= 100_000;
2827
+ const inlineText = inlineEligible
2828
+ ? new TextDecoder("utf-8", { fatal: false }).decode(args.bytes)
2829
+ : undefined;
2830
+ await runtime.client.mutation(api.functions.files.commitFileUpload, {
2831
+ path: args.path,
2832
+ storageId,
2833
+ contentHash: hash,
2834
+ size: args.bytes.byteLength,
2835
+ text: inlineText,
2836
+ lastModifiedBy: "cli",
2837
+ apiKey,
2838
+ } as never);
2839
+ return { path: args.path, storageId, contentHash: hash };
2840
+ }
2841
+
2842
+ async function runFilesLs() {
2843
+ const runtime = await getRuntime();
2844
+ const apiKey = requireFilesApiKey(runtime, "dench files ls");
2845
+ const target = normalizeFilesPath(positional(2) ?? "/");
2846
+ const recursive = hasFlag("--recursive") || hasFlag("-r");
2847
+ const rows = recursive
2848
+ ? await listConvexTreeRecursive(runtime, apiKey, target)
2849
+ : await listConvexTreeAt(runtime, apiKey, target);
2850
+ if (hasFlag("--json")) {
2851
+ print({
2852
+ ok: true,
2853
+ prefix: target,
2854
+ recursive,
2855
+ count: rows.length,
2856
+ entries: rows.map((row) => ({
2857
+ path: row.path,
2858
+ isDir: row.isDir,
2859
+ size: row.size,
2860
+ mtime: row.mtime,
2861
+ contentHash: row.contentHash,
2862
+ })),
2863
+ });
2864
+ return;
2865
+ }
2866
+ if (rows.length === 0) {
2867
+ process.stdout.write(`(no entries under ${target})\n`);
2868
+ return;
2869
+ }
2870
+ for (const row of rows) {
2871
+ const kind = row.isDir ? "DIR " : "FILE";
2872
+ const size = row.isDir
2873
+ ? " -"
2874
+ : (row.size ?? 0).toString().padStart(8, " ");
2875
+ process.stdout.write(`${kind} ${size} ${row.path}\n`);
2876
+ }
2877
+ }
2878
+
2879
+ async function runFilesMv() {
2880
+ const runtime = await getRuntime();
2881
+ const apiKey = requireFilesApiKey(runtime, "dench files mv");
2882
+ const fromRaw = positional(2);
2883
+ const toRaw = positional(3);
2884
+ if (!fromRaw || !toRaw) {
2885
+ throw new CliError("Usage: dench files mv <src> <dst>", {
2886
+ code: "files_mv_usage",
2887
+ nextActions: ["Pass both source and destination paths."],
2888
+ });
2889
+ }
2890
+ const from = normalizeFilesPath(fromRaw);
2891
+ const to = normalizeFilesPath(toRaw);
2892
+ if (from === to) {
2893
+ throw new CliError("Source and destination are the same path", {
2894
+ code: "files_mv_noop",
2895
+ });
2896
+ }
2897
+
2898
+ const sourceRows = await listConvexTreeRecursive(runtime, apiKey, from);
2899
+ const sourceSelf = (await listConvexTreeAt(runtime, apiKey, parentOfPath(from))).find(
2900
+ (row) => row.path === from,
2901
+ );
2902
+ if (!sourceSelf && sourceRows.length === 0) {
2903
+ throw new CliError(`Source not found in Convex: ${from}`, {
2904
+ code: "files_mv_not_found",
2905
+ nextActions: [
2906
+ "Run `dench files ls` on the parent directory to confirm.",
2907
+ "If the file only exists on the sandbox FS, run the daemon (or `dench fs sync --initial`) first.",
2908
+ ],
2909
+ });
2910
+ }
2911
+
2912
+ const moved: Array<{ from: string; to: string }> = [];
2913
+ if (sourceSelf && !sourceSelf.isDir) {
2914
+ const bytes = await downloadConvexFileBytes(runtime, apiKey, from);
2915
+ if (!bytes) {
2916
+ throw new CliError(`Could not download bytes for ${from}`, {
2917
+ code: "files_mv_download",
2918
+ });
2919
+ }
2920
+ await uploadConvexFileBytes(runtime, apiKey, { path: to, bytes });
2921
+ await runtime.client.mutation(api.functions.files.deleteFile, {
2922
+ path: from,
2923
+ apiKey,
2924
+ } as never);
2925
+ moved.push({ from, to });
2926
+ } else {
2927
+ for (const row of sourceRows) {
2928
+ if (row.isDir) continue;
2929
+ const rel = row.path.slice(from.length).replace(/^\/+/, "");
2930
+ const dest = rel ? `${to}/${rel}` : to;
2931
+ const bytes = await downloadConvexFileBytes(runtime, apiKey, row.path);
2932
+ if (!bytes) continue;
2933
+ await uploadConvexFileBytes(runtime, apiKey, { path: dest, bytes });
2934
+ await runtime.client.mutation(api.functions.files.deleteFile, {
2935
+ path: row.path,
2936
+ apiKey,
2937
+ } as never);
2938
+ moved.push({ from: row.path, to: dest });
2939
+ }
2940
+ if (sourceSelf?.isDir) {
2941
+ await runtime.client.mutation(api.functions.files.deleteFile, {
2942
+ path: from,
2943
+ apiKey,
2944
+ } as never);
2945
+ }
2946
+ }
2947
+
2948
+ print({ ok: true, from, to, moved });
2949
+ }
2950
+
2951
+ async function runFilesRm() {
2952
+ const runtime = await getRuntime();
2953
+ const apiKey = requireFilesApiKey(runtime, "dench files rm");
2954
+ const targetRaw = positional(2);
2955
+ if (!targetRaw) {
2956
+ throw new CliError("Usage: dench files rm <path> [--recursive]", {
2957
+ code: "files_rm_usage",
2958
+ });
2959
+ }
2960
+ const target = normalizeFilesPath(targetRaw);
2961
+ const recursive = hasFlag("--recursive") || hasFlag("-r");
2962
+
2963
+ const parentRows = await listConvexTreeAt(runtime, apiKey, parentOfPath(target));
2964
+ const self = parentRows.find((row) => row.path === target);
2965
+ if (!self) {
2966
+ if (hasFlag("--json")) {
2967
+ print({ ok: true, path: target, removedFiles: 0, removedDirs: 0 });
2968
+ } else {
2969
+ process.stdout.write(
2970
+ `dench files rm: ${target} not present in Convex (already absent)\n`,
2971
+ );
2972
+ }
2973
+ return;
2974
+ }
2975
+ if (self.isDir && !recursive) {
2976
+ throw new CliError(
2977
+ `${target} is a directory; pass --recursive to delete it.`,
2978
+ { code: "files_rm_recursive" },
2979
+ );
2980
+ }
2981
+
2982
+ const toDelete = self.isDir
2983
+ ? await listConvexTreeRecursive(runtime, apiKey, target)
2984
+ : [];
2985
+ const fileRows = toDelete.filter((row) => !row.isDir);
2986
+ const dirRows = toDelete.filter((row) => row.isDir);
2987
+ // Children first, parents last.
2988
+ dirRows.sort((a, b) => b.path.length - a.path.length);
2989
+
2990
+ for (const row of fileRows) {
2991
+ await runtime.client.mutation(api.functions.files.deleteFile, {
2992
+ path: row.path,
2993
+ apiKey,
2994
+ } as never);
2995
+ }
2996
+ for (const row of dirRows) {
2997
+ await runtime.client.mutation(api.functions.files.deleteFile, {
2998
+ path: row.path,
2999
+ apiKey,
3000
+ } as never);
3001
+ }
3002
+ await runtime.client.mutation(api.functions.files.deleteFile, {
3003
+ path: target,
3004
+ apiKey,
3005
+ } as never);
3006
+
3007
+ print({
3008
+ ok: true,
3009
+ path: target,
3010
+ removedFiles: fileRows.length + (self.isDir ? 0 : 1),
3011
+ removedDirs: dirRows.length + (self.isDir ? 1 : 0),
3012
+ });
3013
+ }
3014
+
3015
+ async function runFilesCommand() {
3016
+ const sub = positional(1);
3017
+ if (!sub || sub === "help" || hasFlag("--help")) {
3018
+ process.stdout.write(
3019
+ [
3020
+ "dench files <ls|mv|rm> — workspace file ops that update the UI tree",
3021
+ "",
3022
+ " dench files ls [<path>] [--recursive] [--json]",
3023
+ " dench files mv <src> <dst> [--json]",
3024
+ " dench files rm <path> [--recursive] [--json]",
3025
+ "",
3026
+ "Paths are POSIX paths under /workspace; you can pass either form.",
3027
+ "These mutate Convex directly (and the daemon syncs to the volume).",
3028
+ ].join("\n"),
3029
+ );
3030
+ return;
3031
+ }
3032
+ if (sub === "ls") return await runFilesLs();
3033
+ if (sub === "mv") return await runFilesMv();
3034
+ if (sub === "rm") return await runFilesRm();
3035
+ throw new CliError(`Unknown subcommand: dench files ${sub}`, {
3036
+ code: "files_unknown_subcommand",
3037
+ nextActions: ["Run dench files help for usage."],
3038
+ });
3039
+ }
3040
+
3041
+ function parentOfPath(input: string): string {
3042
+ const normalized = normalizeFilesPath(input);
3043
+ if (normalized === "/") return "/";
3044
+ const idx = normalized.lastIndexOf("/");
3045
+ return idx <= 0 ? "/" : normalized.slice(0, idx);
3046
+ }
3047
+
3048
+ function sha256HexSync(bytes: Uint8Array): string {
3049
+ // Used by the dench files commands so we can compute the hash
3050
+ // alongside the upload synchronously. Bun + Node both ship
3051
+ // node:crypto, so this is free.
3052
+ const { createHash } = require("node:crypto") as typeof import("node:crypto");
3053
+ return createHash("sha256").update(bytes).digest("hex");
3054
+ }
3055
+
2545
3056
  async function spawnInherited(binary: string, commandArgs: string[]) {
2546
3057
  return await new Promise<number>((resolve, reject) => {
2547
3058
  const child = spawn(binary, commandArgs, {
@@ -2675,14 +3186,16 @@ async function main() {
2675
3186
  }
2676
3187
  const { runCrmCommand } = await import("./crm");
2677
3188
  const runtime = await getRuntime();
2678
- const sessionRuntime = requireSessionRuntime(runtime, "dench crm");
3189
+ // CRM is org-scoped accept either an agent session token
3190
+ // (`dch_agent_*` from `dench login`) or a unified Dench API key
3191
+ // (`DENCH_API_KEY` baked into sandbox envVars). Server-side
3192
+ // `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
3193
+ // bearer prefix and resolves the org for both.
3194
+ const authedRuntime = requireAuthenticatedRuntime(runtime, "dench crm");
2679
3195
  await runCrmCommand({
2680
- convex: sessionRuntime.client,
3196
+ convex: authedRuntime.client,
2681
3197
  args: subArgs,
2682
- // Server-side `requireCrmAccess` (convex/lib/crm/access.ts) accepts
2683
- // this token and resolves the agent's organization without a Convex
2684
- // Auth cookie. Mirrors the pattern used by every agentWorkspace fn.
2685
- sessionToken: sessionRuntime.sessionToken,
3198
+ sessionToken: authedRuntime.sessionToken,
2686
3199
  });
2687
3200
  return;
2688
3201
  }
@@ -2692,6 +3205,60 @@ async function main() {
2692
3205
  return;
2693
3206
  }
2694
3207
 
3208
+ if (command === "files") {
3209
+ await runFilesCommand();
3210
+ return;
3211
+ }
3212
+
3213
+ if (command === "chat") {
3214
+ const subArgs = args.slice(args.indexOf("chat") + 1);
3215
+ const sub = subArgs[0];
3216
+ if (!sub || sub === "help" || sub === "--help") {
3217
+ const { runChatCommand } = await import("./chat");
3218
+ // biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
3219
+ await runChatCommand({ convex: {} as any, args: subArgs });
3220
+ return;
3221
+ }
3222
+ // `chat new` / `chat send` spawn a chat-turn workflow via
3223
+ // /api/chat/cli (the same workflow the web UI uses). They need
3224
+ // an HTTP host + bearer token, not a Convex client. The
3225
+ // remaining subcommands (list / search / read) are Convex
3226
+ // queries and keep going through `runChatCommand`.
3227
+ if (sub === "new" || sub === "send") {
3228
+ const { runChatSpawnCommand } = await import("./chat-spawn");
3229
+ const runtime = await getRuntime();
3230
+ const authedRuntime = requireAuthenticatedRuntime(
3231
+ runtime,
3232
+ `dench chat ${sub}`,
3233
+ );
3234
+ // Drop the leading subcommand from subArgs — runChatSpawnCommand
3235
+ // expects the remaining tokens (prompt + flags).
3236
+ await runChatSpawnCommand({
3237
+ runtime: {
3238
+ host: authedRuntime.host,
3239
+ bearerToken: authedRuntime.sessionToken,
3240
+ },
3241
+ args: subArgs.slice(1),
3242
+ subcommand: sub,
3243
+ });
3244
+ return;
3245
+ }
3246
+ const { runChatCommand } = await import("./chat");
3247
+ const runtime = await getRuntime();
3248
+ // Past-chat queries are org+user-scoped: the server resolves the
3249
+ // user from the session token, OR from `runId` when authenticating
3250
+ // with a Dench API key. Pass DENCH_RUN_ID through automatically so
3251
+ // `dench chat …` works inside a sandbox without extra flags.
3252
+ const authedRuntime = requireAuthenticatedRuntime(runtime, "dench chat");
3253
+ await runChatCommand({
3254
+ convex: authedRuntime.client,
3255
+ args: subArgs,
3256
+ sessionToken: authedRuntime.sessionToken,
3257
+ runId: process.env.DENCH_RUN_ID?.trim() || undefined,
3258
+ });
3259
+ return;
3260
+ }
3261
+
2695
3262
  if (command === "agent") {
2696
3263
  const subArgs = args.slice(args.indexOf("agent") + 1);
2697
3264
  const sub = subArgs[0];
@@ -2701,6 +3268,27 @@ async function main() {
2701
3268
  await runAgentCommand({ convex: {} as any, args: subArgs });
2702
3269
  return;
2703
3270
  }
3271
+ // Aliases: `dench agent new` / `dench agent send` -> the chat-spawn
3272
+ // implementation. Surfaced under `agent` so users who associate
3273
+ // chat threads with the agent surface still find them; the actual
3274
+ // workflow is identical to `dench chat new` / `dench chat send`.
3275
+ if (sub === "new" || sub === "send") {
3276
+ const { runChatSpawnCommand } = await import("./chat-spawn");
3277
+ const runtime = await getRuntime();
3278
+ const authedRuntime = requireAuthenticatedRuntime(
3279
+ runtime,
3280
+ `dench agent ${sub}`,
3281
+ );
3282
+ await runChatSpawnCommand({
3283
+ runtime: {
3284
+ host: authedRuntime.host,
3285
+ bearerToken: authedRuntime.sessionToken,
3286
+ },
3287
+ args: subArgs.slice(1),
3288
+ subcommand: sub,
3289
+ });
3290
+ return;
3291
+ }
2704
3292
  const { runAgentCommand } = await import("./agent");
2705
3293
  const runtime = await getRuntime();
2706
3294
  await runAgentCommand({
@@ -2881,6 +3469,13 @@ async function main() {
2881
3469
  print(status);
2882
3470
  return;
2883
3471
  }
3472
+ if (runtime.mode === "apiKey") {
3473
+ // No agent session → no agent-status query. Fall back to a thin
3474
+ // env-derived snapshot so sandbox callers still get something
3475
+ // useful from `dench status`.
3476
+ print(await buildContext(runtime));
3477
+ return;
3478
+ }
2884
3479
  const data = await devOverview(runtime);
2885
3480
  const output = scopedStatus
2886
3481
  ? summarizeDevMine(runtime.workspaceArgs.workspaceSlug, data)
@@ -2903,6 +3498,11 @@ async function main() {
2903
3498
  );
2904
3499
  return;
2905
3500
  }
3501
+ if (runtime.mode === "apiKey") {
3502
+ throw loginRequiredError(
3503
+ "dench tasks (agent-scoped task list)",
3504
+ );
3505
+ }
2906
3506
  const data = await devOverview(runtime);
2907
3507
  print(data.tasks);
2908
3508
  return;
@@ -2919,6 +3519,9 @@ async function main() {
2919
3519
  print(status.agents);
2920
3520
  return;
2921
3521
  }
3522
+ if (runtime.mode === "apiKey") {
3523
+ throw loginRequiredError("dench agents");
3524
+ }
2922
3525
  const data = await devOverview(runtime);
2923
3526
  print(data.agents);
2924
3527
  return;
@@ -2935,6 +3538,9 @@ async function main() {
2935
3538
  print(status.approvals);
2936
3539
  return;
2937
3540
  }
3541
+ if (runtime.mode === "apiKey") {
3542
+ throw loginRequiredError("dench approvals");
3543
+ }
2938
3544
  const data = await devOverview(runtime);
2939
3545
  print(data.approvals);
2940
3546
  return;
@@ -3000,6 +3606,7 @@ async function main() {
3000
3606
  );
3001
3607
  return;
3002
3608
  }
3609
+ rejectApiKeyForAgentCommand(runtime, "dench task create");
3003
3610
  const taskId = await runtime.client.mutation(
3004
3611
  api.functions.agentWorkspace.devCreateTask,
3005
3612
  {
@@ -3108,6 +3715,7 @@ async function main() {
3108
3715
  );
3109
3716
  return;
3110
3717
  }
3718
+ rejectApiKeyForAgentCommand(runtime, "dench claim");
3111
3719
  const agentId = option("--agent") ?? (await defaultDevAgentId(runtime));
3112
3720
  print(
3113
3721
  await runtime.client.mutation(api.functions.agentWorkspace.devClaimTask, {
@@ -3135,6 +3743,7 @@ async function main() {
3135
3743
  );
3136
3744
  return;
3137
3745
  }
3746
+ rejectApiKeyForAgentCommand(runtime, "dench log");
3138
3747
  print(
3139
3748
  await runtime.client.mutation(api.functions.agentWorkspace.devAppendLog, {
3140
3749
  ...runtime.workspaceArgs,
@@ -3164,6 +3773,7 @@ async function main() {
3164
3773
  );
3165
3774
  return;
3166
3775
  }
3776
+ rejectApiKeyForAgentCommand(runtime, "dench approval request");
3167
3777
  const approvalId = await runtime.client.mutation(
3168
3778
  api.functions.agentWorkspace.devRequestApproval,
3169
3779
  {