@dench.com/cli 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/browser.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * `dench browser` — CLI control for the cloud Browserbase browser.
2
+ * `dench browser` — CLI control for Dench Browser.
3
3
  *
4
- * The CLI does not launch Chrome locally and never receives Browserbase
5
- * credentials. It sends a bearer-authenticated JSON command to
6
- * /api/browser/cli, where Dench Cloud drives Browserbase server-side.
4
+ * The CLI does not launch Chrome locally and never receives provider
5
+ * credentials. It sends bearer-authenticated JSON commands to Dench Cloud.
7
6
  */
8
7
 
9
8
  import { createHash } from "node:crypto";
@@ -15,6 +14,9 @@ import { hasFlag } from "./lib/cli-args";
15
14
  type RuntimeBundle = {
16
15
  host: string;
17
16
  bearerToken: string;
17
+ gatewayBaseUrl?: string;
18
+ gatewayBearerToken?: string;
19
+ gatewayHeaders?: Record<string, string>;
18
20
  };
19
21
 
20
22
  type BrowserCliContext = {
@@ -46,6 +48,15 @@ type BrowserResponse = Record<string, unknown> & {
46
48
  threadUrl?: string;
47
49
  };
48
50
 
51
+ type DenchBrowserSessionResponse = {
52
+ provider?: string;
53
+ sessionId?: string;
54
+ connectUrl?: string;
55
+ liveViewUrl?: string;
56
+ status?: string;
57
+ contextId?: string;
58
+ };
59
+
49
60
  class BrowserCliError extends Error {}
50
61
 
51
62
  const ACTIONS = new Set([
@@ -72,7 +83,7 @@ const ACTIONS = new Set([
72
83
  function browserHelp(): void {
73
84
  console.log(`Usage: dench browser <action> [options]
74
85
 
75
- Control the cloud Browserbase browser without opening dench.com locally.
86
+ Control Dench Browser without opening dench.com locally.
76
87
  Commands authenticate with the active dench signin session or DENCH_API_KEY.
77
88
 
78
89
  Common:
@@ -95,6 +106,11 @@ Common:
95
106
  dench browser close-tab <targetId>
96
107
  dench browser stop
97
108
 
109
+ Playwright sessions:
110
+ dench browser session create --json
111
+ dench browser session status <sessionId> --json
112
+ dench browser session stop <sessionId> --json
113
+
98
114
  State:
99
115
  The CLI remembers the latest browser thread per Dench host/session.
100
116
  Use --thread <threadId> to control a specific thread, or
@@ -213,10 +229,54 @@ function buildCliBrowserUrl(host: string): string {
213
229
  return `${host.replace(/\/+$/, "")}/api/browser/cli`;
214
230
  }
215
231
 
232
+ function normalizeGatewayBaseUrl(value: string | undefined): string {
233
+ const explicit = value?.trim();
234
+ if (explicit) return explicit.replace(/\/+$/, "").replace(/\/v1$/, "");
235
+ if ("DENCH_GATEWAY_URL" in process.env || "GATEWAY_URL" in process.env) {
236
+ const env =
237
+ process.env.DENCH_GATEWAY_URL?.trim() || process.env.GATEWAY_URL?.trim();
238
+ if (env) return env.replace(/\/+$/, "").replace(/\/v1$/, "");
239
+ throw new BrowserCliError(
240
+ "DENCH_GATEWAY_URL is set but empty — configure a gateway URL or unset the variable",
241
+ );
242
+ }
243
+ if (/localhost|127\.0\.0\.1/.test(process.env.DENCH_HOST ?? "")) {
244
+ return "http://localhost:8787";
245
+ }
246
+ return "https://gateway.merseoriginals.com";
247
+ }
248
+
249
+ function buildGatewayBrowserUrl(
250
+ runtime: RuntimeBundle,
251
+ path = "/v1/browser/sessions",
252
+ ): string {
253
+ return `${normalizeGatewayBaseUrl(runtime.gatewayBaseUrl)}${path}`;
254
+ }
255
+
256
+ function buildRegisterSessionUrl(runtime: RuntimeBundle): string {
257
+ const convex = (
258
+ process.env.CONVEX_URL ||
259
+ process.env.NEXT_PUBLIC_CONVEX_URL ||
260
+ ""
261
+ ).trim();
262
+ if (convex) {
263
+ const siteBase = convex
264
+ .replace(/\/+$/, "")
265
+ .replace(/\.convex\.cloud$/i, ".convex.site");
266
+ return `${siteBase}/api/browser/register-session`;
267
+ }
268
+ const apiBase =
269
+ process.env.DENCH_API_URL?.trim() ||
270
+ process.env.DENCH_HOST?.trim() ||
271
+ runtime.host;
272
+ return `${apiBase.replace(/\/+$/, "")}/api/browser/register-session`;
273
+ }
274
+
216
275
  async function postJson(
217
276
  url: string,
218
277
  bearer: string,
219
278
  body: unknown,
279
+ extraHeaders: Record<string, string> = {},
220
280
  ): Promise<{ ok: boolean; payload: unknown; status: number }> {
221
281
  const response = await fetch(url, {
222
282
  method: "POST",
@@ -224,6 +284,7 @@ async function postJson(
224
284
  accept: "application/json",
225
285
  authorization: `Bearer ${bearer}`,
226
286
  "content-type": "application/json",
287
+ ...extraHeaders,
227
288
  },
228
289
  body: JSON.stringify(body),
229
290
  });
@@ -237,14 +298,223 @@ async function postJson(
237
298
  return { ok: response.ok, payload, status: response.status };
238
299
  }
239
300
 
301
+ async function getJson(
302
+ url: string,
303
+ bearer: string,
304
+ extraHeaders: Record<string, string> = {},
305
+ ): Promise<{ ok: boolean; payload: unknown; status: number }> {
306
+ const response = await fetch(url, {
307
+ method: "GET",
308
+ headers: {
309
+ accept: "application/json",
310
+ authorization: `Bearer ${bearer}`,
311
+ ...extraHeaders,
312
+ },
313
+ });
314
+ const text = await response.text();
315
+ let payload: unknown;
316
+ try {
317
+ payload = text ? JSON.parse(text) : null;
318
+ } catch {
319
+ payload = { raw: text };
320
+ }
321
+ return { ok: response.ok, payload, status: response.status };
322
+ }
323
+
324
+ async function deleteJson(
325
+ url: string,
326
+ bearer: string,
327
+ extraHeaders: Record<string, string> = {},
328
+ ): Promise<{ ok: boolean; payload: unknown; status: number }> {
329
+ const response = await fetch(url, {
330
+ method: "DELETE",
331
+ headers: {
332
+ accept: "application/json",
333
+ authorization: `Bearer ${bearer}`,
334
+ ...extraHeaders,
335
+ },
336
+ });
337
+ const text = await response.text();
338
+ let payload: unknown;
339
+ try {
340
+ payload = text ? JSON.parse(text) : null;
341
+ } catch {
342
+ payload = { raw: text };
343
+ }
344
+ return { ok: response.ok, payload, status: response.status };
345
+ }
346
+
240
347
  function payloadError(payload: unknown): string {
241
348
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
242
349
  const error = (payload as { error?: unknown }).error;
243
350
  if (typeof error === "string" && error.trim()) return error;
351
+ if (error && typeof error === "object" && !Array.isArray(error)) {
352
+ const message = (error as { message?: unknown }).message;
353
+ if (typeof message === "string" && message.trim()) return message;
354
+ }
244
355
  }
245
356
  return "Cloud browser request failed";
246
357
  }
247
358
 
359
+ function gatewayBearerToken(runtime: RuntimeBundle): string {
360
+ return runtime.gatewayBearerToken?.trim() || runtime.bearerToken;
361
+ }
362
+
363
+ function gatewayBrowserHeaders(runtime: RuntimeBundle): Record<string, string> {
364
+ const runId = process.env.DENCH_RUN_ID?.trim();
365
+ return {
366
+ ...(runtime.gatewayHeaders ?? {}),
367
+ ...(runId ? { "x-dench-run-id": runId } : {}),
368
+ };
369
+ }
370
+
371
+ async function registerScriptSession(args: {
372
+ runtime: RuntimeBundle;
373
+ session: DenchBrowserSessionResponse;
374
+ status: string;
375
+ }): Promise<void> {
376
+ const runId = process.env.DENCH_RUN_ID?.trim();
377
+ const sessionId = args.session.sessionId?.trim();
378
+ if (!runId || !sessionId) return;
379
+ try {
380
+ await postJson(
381
+ buildRegisterSessionUrl(args.runtime),
382
+ args.runtime.bearerToken,
383
+ {
384
+ runId,
385
+ sessionId,
386
+ liveViewUrl: args.session.liveViewUrl,
387
+ status: args.status,
388
+ },
389
+ );
390
+ } catch {
391
+ // Best-effort: browser automation must not fail because UI registration
392
+ // could not link the session to the run.
393
+ }
394
+ }
395
+
396
+ function parseSessionCreateBody(args: string[]): Record<string, unknown> {
397
+ // The per-user browser profile is resolved server-side by the gateway
398
+ // from the authenticated Dench API key plus DENCH_RUN_ID when present
399
+ // (Convex `browserIdentities`).
400
+ // `--context-id` stays only as an explicit override for manual testing;
401
+ // there is no env-var fallback.
402
+ const contextId =
403
+ consumeFlagValue(args, "--context-id") ||
404
+ consumeFlagValue(args, "--contextId") ||
405
+ "";
406
+ const width = parseNumberFlag(args, "--width");
407
+ const height = parseNumberFlag(args, "--height");
408
+ return {
409
+ ...(contextId ? { contextId } : {}),
410
+ ...(width || height
411
+ ? {
412
+ viewport: {
413
+ ...(width ? { width } : {}),
414
+ ...(height ? { height } : {}),
415
+ },
416
+ }
417
+ : {}),
418
+ };
419
+ }
420
+
421
+ async function runSessionCommand(ctx: BrowserCliContext): Promise<void> {
422
+ ctx.args.shift(); // session
423
+ const subcommand = ctx.args.shift() ?? "create";
424
+ const bearerToken = gatewayBearerToken(ctx.runtime);
425
+ const gatewayHeaders = gatewayBrowserHeaders(ctx.runtime);
426
+
427
+ if (subcommand === "create") {
428
+ const body = parseSessionCreateBody(ctx.args);
429
+ const result = await postJson(
430
+ buildGatewayBrowserUrl(ctx.runtime),
431
+ bearerToken,
432
+ body,
433
+ gatewayHeaders,
434
+ );
435
+ if (!result.ok) {
436
+ throw new BrowserCliError(
437
+ `${payloadError(result.payload)} (${result.status})`,
438
+ );
439
+ }
440
+ const session =
441
+ result.payload && typeof result.payload === "object"
442
+ ? (result.payload as DenchBrowserSessionResponse)
443
+ : {};
444
+ await registerScriptSession({
445
+ runtime: ctx.runtime,
446
+ session,
447
+ status: session.status ?? "live",
448
+ });
449
+ output(
450
+ ctx,
451
+ ctx.jsonOutput ? session : `Browser session ${session.sessionId}`,
452
+ );
453
+ return;
454
+ }
455
+
456
+ if (subcommand === "status") {
457
+ const sessionId = ctx.args[0]?.trim();
458
+ if (!sessionId) {
459
+ throw new BrowserCliError(
460
+ "dench browser session status requires a session id",
461
+ );
462
+ }
463
+ const result = await getJson(
464
+ buildGatewayBrowserUrl(
465
+ ctx.runtime,
466
+ `/v1/browser/sessions/${encodeURIComponent(sessionId)}`,
467
+ ),
468
+ bearerToken,
469
+ gatewayHeaders,
470
+ );
471
+ if (!result.ok) {
472
+ throw new BrowserCliError(
473
+ `${payloadError(result.payload)} (${result.status})`,
474
+ );
475
+ }
476
+ output(
477
+ ctx,
478
+ ctx.jsonOutput ? result.payload : JSON.stringify(result.payload, null, 2),
479
+ );
480
+ return;
481
+ }
482
+
483
+ if (subcommand === "stop") {
484
+ const sessionId = ctx.args[0]?.trim();
485
+ if (!sessionId) {
486
+ throw new BrowserCliError(
487
+ "dench browser session stop requires a session id",
488
+ );
489
+ }
490
+ const result = await deleteJson(
491
+ buildGatewayBrowserUrl(
492
+ ctx.runtime,
493
+ `/v1/browser/sessions/${encodeURIComponent(sessionId)}`,
494
+ ),
495
+ bearerToken,
496
+ gatewayHeaders,
497
+ );
498
+ if (!result.ok) {
499
+ throw new BrowserCliError(
500
+ `${payloadError(result.payload)} (${result.status})`,
501
+ );
502
+ }
503
+ await registerScriptSession({
504
+ runtime: ctx.runtime,
505
+ session: { sessionId },
506
+ status: "ended",
507
+ });
508
+ output(
509
+ ctx,
510
+ ctx.jsonOutput ? result.payload : `Stopped browser session ${sessionId}`,
511
+ );
512
+ return;
513
+ }
514
+
515
+ throw new BrowserCliError(`Unknown browser session command: ${subcommand}`);
516
+ }
517
+
248
518
  async function maybeWriteScreenshot(
249
519
  response: BrowserResponse,
250
520
  outputPath: string | undefined,
@@ -459,6 +729,11 @@ export async function runBrowserCommand(opts: {
459
729
  return;
460
730
  }
461
731
 
732
+ if (first === "session") {
733
+ await runSessionCommand(ctx);
734
+ return;
735
+ }
736
+
462
737
  const { body, outputPath } = parseActionAndBody(ctx);
463
738
  if (!body.threadId) {
464
739
  body.threadId = await readRememberedThreadId(opts.runtime);
package/dench.ts CHANGED
@@ -501,7 +501,7 @@ Live web search (Exa via the Dench Gateway, auths with the active \`dench signin
501
501
  dench search answer "<query>" [--json]
502
502
  Help: dench search help
503
503
 
504
- Cloud browser (Browserbase, runs entirely in Dench Cloud):
504
+ Cloud browser (runs entirely in Dench Cloud):
505
505
  dench browser open [url] [--json]
506
506
  dench browser inspect [--json]
507
507
  dench browser navigate <url>
@@ -4053,11 +4053,19 @@ async function main() {
4053
4053
  const { runBrowserCommand } = await import("./browser");
4054
4054
  const runtime = await getRuntime();
4055
4055
  const authedRuntime = requireAuthenticatedRuntime(runtime, "dench browser");
4056
+ const filteredSubArgs = subArgs.filter((a) => a !== "--json");
4057
+ const gatewayAuth =
4058
+ filteredSubArgs[0] === "session"
4059
+ ? await resolveGatewayCommandAuth(runtime, "dench browser session")
4060
+ : undefined;
4056
4061
  await runBrowserCommand({
4057
4062
  args: subArgs,
4058
4063
  runtime: {
4059
4064
  host: authedRuntime.host,
4060
4065
  bearerToken: authedRuntime.sessionToken,
4066
+ gatewayBearerToken: gatewayAuth?.bearerToken,
4067
+ gatewayBaseUrl: gatewayAuth?.gatewayBaseUrl,
4068
+ gatewayHeaders: gatewayAuth?.gatewayHeaders,
4061
4069
  },
4062
4070
  });
4063
4071
  return;
@@ -2532,6 +2532,13 @@ export const convexExtras: Record<string, ConvexExtras> = {
2532
2532
  // ---------------------------------------------------------------------------
2533
2533
 
2534
2534
  export const requestExamples: Record<string, unknown> = {
2535
+ "browser.action": {
2536
+ action: "open",
2537
+ url: "https://example.com",
2538
+ },
2539
+ "browser.session.create": {
2540
+ viewport: { width: 1280, height: 800 },
2541
+ },
2535
2542
  "crm.objects.create": {
2536
2543
  name: "lead",
2537
2544
  description: "Sales leads",
@@ -2703,6 +2710,14 @@ const workspaceStatusExample = {
2703
2710
  };
2704
2711
 
2705
2712
  export const responseExamples: Record<string, unknown> = {
2713
+ "browser.session.create": {
2714
+ provider: "dench_browser",
2715
+ sessionId: "bb_sess_123",
2716
+ connectUrl:
2717
+ "wss://gateway.merseoriginals.com/v1/browser/sessions/bb_sess_123/cdp?relay_token=...",
2718
+ liveViewUrl: "https://www.dench.com/dench?...",
2719
+ status: "live",
2720
+ },
2706
2721
  "commands.list": {
2707
2722
  operations: [
2708
2723
  {
@@ -34,6 +34,7 @@ export type CustomOperation = {
34
34
  | "upgrade"
35
35
  | "filesMove"
36
36
  | "filesStage"
37
+ | "browserAction"
37
38
  | "signinOtpStart"
38
39
  | "signinOtpVerify"
39
40
  | "signinOtpFinalize";
@@ -1963,6 +1964,57 @@ const rawApiOperations = [
1963
1964
  backend: gateway("DELETE", "/composio/connections/{connectionId}"),
1964
1965
  }),
1965
1966
 
1967
+ // Browser automation (Dench Browser). The provider stays server-side; the
1968
+ // per-(org,user) browser profile is resolved by the gateway, so callers never
1969
+ // see or send provider credentials or context ids.
1970
+ op({
1971
+ id: "browser.action",
1972
+ group: "browser",
1973
+ summary:
1974
+ "Drive Dench Browser with a single interactive action (open, inspect, click, fill, type, screenshot, stop). Pass the step in `action` plus its arguments.",
1975
+ cli: "dench browser",
1976
+ method: "POST",
1977
+ path: "/browser/cli",
1978
+ requestSchema: anyObject,
1979
+ responseSchema: anyResponse,
1980
+ backend: custom("browserAction", "bearer"),
1981
+ }),
1982
+ op({
1983
+ id: "browser.session.create",
1984
+ group: "browser",
1985
+ summary:
1986
+ "Create a Dench Browser session and return a CDP connect URL for Playwright (chromium.connectOverCDP) plus a live-view URL. Use for durable scripts run from the sandbox.",
1987
+ cli: "dench browser session create",
1988
+ method: "POST",
1989
+ path: "/browser/sessions",
1990
+ requestSchema: anyObject,
1991
+ responseSchema: anyResponse,
1992
+ backend: gateway("POST", "/browser/sessions"),
1993
+ }),
1994
+ op({
1995
+ id: "browser.session.status",
1996
+ group: "browser",
1997
+ summary:
1998
+ "Get the status, connect URL, and live-view URL for a Dench Browser session.",
1999
+ cli: "dench browser session status",
2000
+ method: "GET",
2001
+ path: "/browser/sessions/{sessionId}",
2002
+ requestSchema: anyObject,
2003
+ responseSchema: anyResponse,
2004
+ backend: gateway("GET", "/browser/sessions/{sessionId}"),
2005
+ }),
2006
+ op({
2007
+ id: "browser.session.stop",
2008
+ group: "browser",
2009
+ summary: "Stop a Dench Browser session and release the remote browser.",
2010
+ cli: "dench browser session stop",
2011
+ method: "DELETE",
2012
+ path: "/browser/sessions/{sessionId}",
2013
+ requestSchema: anyObject,
2014
+ responseSchema: anyResponse,
2015
+ backend: gateway("DELETE", "/browser/sessions/{sessionId}"),
2016
+ }),
2017
+
1966
2018
  // Agent harness config.
1967
2019
  op({
1968
2020
  id: "agentConfig.identity.show",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
5
5
  "type": "module",
6
6
  "bin": {