@dench.com/cli 2.1.1 → 2.1.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/crm.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * The full surface mirrors the plan's "dench-cli CRM surface" section:
18
18
  * crm objects list / get / create / update / delete / rename
19
+ * crm views list / get / entries
19
20
  * crm fields list / create / update / delete / reorder
20
21
  * crm entries list / get / create / create-many / update / update-many / delete / bulk-delete
21
22
  * crm cells get / set / set-many / append
@@ -330,6 +331,29 @@ async function callMutation(
330
331
  }
331
332
  }
332
333
 
334
+ async function suppressForwardedConvexLogs<T>(
335
+ fn: () => Promise<T>,
336
+ ): Promise<T> {
337
+ const originalLog = console.log;
338
+ const originalInfo = console.info;
339
+ const shouldSuppress = (args: unknown[]) =>
340
+ typeof args[0] === "string" && args[0].startsWith("[CONVEX ");
341
+ console.log = (...args: Parameters<typeof console.log>) => {
342
+ if (shouldSuppress(args)) return;
343
+ originalLog(...args);
344
+ };
345
+ console.info = (...args: Parameters<typeof console.info>) => {
346
+ if (shouldSuppress(args)) return;
347
+ originalInfo(...args);
348
+ };
349
+ try {
350
+ return await fn();
351
+ } finally {
352
+ console.log = originalLog;
353
+ console.info = originalInfo;
354
+ }
355
+ }
356
+
333
357
  function out(ctx: CrmCliContext, value: unknown): void {
334
358
  if (ctx.jsonOutput) {
335
359
  console.log(JSON.stringify(value, null, 2));
@@ -410,6 +434,9 @@ const api = {
410
434
  objects: {
411
435
  list: makeFunctionReference<"query">("functions/crm/objects:list"),
412
436
  get: makeFunctionReference<"query">("functions/crm/objects:get"),
437
+ materializeView: makeFunctionReference<"query">(
438
+ "functions/crm/objects:materializeView",
439
+ ),
413
440
  create: makeFunctionReference<"mutation">("functions/crm/objects:create"),
414
441
  update: makeFunctionReference<"mutation">("functions/crm/objects:update"),
415
442
  rename: makeFunctionReference<"mutation">("functions/crm/objects:rename"),
@@ -507,6 +534,8 @@ export async function runCrmCommand(opts: {
507
534
  switch (subcommand) {
508
535
  case "objects":
509
536
  return await runObjectsCommand(ctx);
537
+ case "views":
538
+ return await runViewsCommand(ctx);
510
539
  case "fields":
511
540
  return await runFieldsCommand(ctx);
512
541
  case "entries":
@@ -636,6 +665,181 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
636
665
  }
637
666
  }
638
667
 
668
+ type SavedViewForCli = Record<string, unknown> & {
669
+ name?: unknown;
670
+ pinnedEntryIds?: unknown;
671
+ filters?: unknown;
672
+ sort?: unknown;
673
+ };
674
+
675
+ type CrmObjectWithViews = {
676
+ name?: string;
677
+ savedViews?: SavedViewForCli[];
678
+ };
679
+
680
+ function savedViewsFromObject(
681
+ objectName: string,
682
+ object: unknown,
683
+ ): SavedViewForCli[] {
684
+ if (!object || typeof object !== "object") {
685
+ throw new CrmCliError(`CRM object "${objectName}" not found`);
686
+ }
687
+ const savedViews = (object as CrmObjectWithViews).savedViews;
688
+ return Array.isArray(savedViews) ? savedViews : [];
689
+ }
690
+
691
+ function readViewName(view: SavedViewForCli): string | null {
692
+ return typeof view.name === "string" && view.name.trim()
693
+ ? view.name.trim()
694
+ : null;
695
+ }
696
+
697
+ function resolveSavedView(
698
+ objectName: string,
699
+ views: SavedViewForCli[],
700
+ requestedName: string,
701
+ ): SavedViewForCli {
702
+ const trimmed = requestedName.trim();
703
+ const exact = views.find((view) => readViewName(view) === trimmed);
704
+ if (exact) return exact;
705
+
706
+ const lower = trimmed.toLowerCase();
707
+ const insensitiveMatches = views.filter(
708
+ (view) => readViewName(view)?.toLowerCase() === lower,
709
+ );
710
+ if (insensitiveMatches.length === 1) return insensitiveMatches[0];
711
+ if (insensitiveMatches.length > 1) {
712
+ throw new CrmCliError(
713
+ `Multiple views named like "${requestedName}" exist on ${objectName}; use the exact case-sensitive name.`,
714
+ );
715
+ }
716
+
717
+ const available = views.map(readViewName).filter(Boolean).join(", ");
718
+ throw new CrmCliError(
719
+ `View "${requestedName}" not found on ${objectName}${
720
+ available ? `. Available views: ${available}` : ". No saved views exist."
721
+ }`,
722
+ );
723
+ }
724
+
725
+ function summarizeSavedView(view: SavedViewForCli): Record<string, unknown> {
726
+ const pinned = Array.isArray(view.pinnedEntryIds)
727
+ ? view.pinnedEntryIds
728
+ : undefined;
729
+ return {
730
+ ...view,
731
+ pinnedCount: pinned?.length ?? 0,
732
+ };
733
+ }
734
+
735
+ async function runViewsCommand(ctx: CrmCliContext): Promise<void> {
736
+ const verb = ctx.args.shift();
737
+ switch (verb) {
738
+ case "list": {
739
+ const objectName = shift(ctx.args, "object name");
740
+ assertNoUnknownFlags(ctx.args, "crm views list");
741
+ const object = await callQuery(ctx, api.objects.get, {
742
+ name: objectName,
743
+ });
744
+ const views = savedViewsFromObject(objectName, object).map(
745
+ summarizeSavedView,
746
+ );
747
+ out(ctx, views);
748
+ return;
749
+ }
750
+ case "get": {
751
+ const objectName = shift(ctx.args, "object name");
752
+ const viewName = shift(ctx.args, "view name");
753
+ assertNoUnknownFlags(ctx.args, "crm views get");
754
+ const object = await callQuery(ctx, api.objects.get, {
755
+ name: objectName,
756
+ });
757
+ const view = resolveSavedView(
758
+ objectName,
759
+ savedViewsFromObject(objectName, object),
760
+ viewName,
761
+ );
762
+ out(ctx, summarizeSavedView(view));
763
+ return;
764
+ }
765
+ case "entries": {
766
+ const objectName = shift(ctx.args, "object name");
767
+ const viewName = shift(ctx.args, "view name");
768
+ const page = Number.parseInt(getFlag(ctx.args, "--page") ?? "1", 10);
769
+ const pageSize = Number.parseInt(
770
+ getFlagWithAliases(ctx.args, "--page-size", ["--limit"]) ?? "100",
771
+ 10,
772
+ );
773
+ const withMeta = hasFlag(ctx.args, "--with-meta");
774
+ assertNoUnknownFlags(ctx.args, "crm views entries");
775
+ if (!Number.isFinite(page) || page < 1) {
776
+ throw new CrmCliError(`--page must be a positive integer, got ${page}`);
777
+ }
778
+ if (!Number.isFinite(pageSize) || pageSize < 1) {
779
+ throw new CrmCliError(
780
+ `--page-size/--limit must be a positive integer, got ${pageSize}`,
781
+ );
782
+ }
783
+
784
+ const object = await callQuery(ctx, api.objects.get, {
785
+ name: objectName,
786
+ });
787
+ const view = resolveSavedView(
788
+ objectName,
789
+ savedViewsFromObject(objectName, object),
790
+ viewName,
791
+ );
792
+ const pinnedEntryIds = Array.isArray(view.pinnedEntryIds)
793
+ ? view.pinnedEntryIds.filter(
794
+ (id): id is string => typeof id === "string" && id.length > 0,
795
+ )
796
+ : undefined;
797
+ const materialized = (await suppressForwardedConvexLogs(() =>
798
+ callQuery(ctx, api.objects.materializeView, {
799
+ objectName,
800
+ page,
801
+ pageSize,
802
+ filters: view.filters,
803
+ sort: Array.isArray(view.sort) ? view.sort : undefined,
804
+ pinnedEntryIds,
805
+ includeReverseRelations: false,
806
+ }),
807
+ )) as {
808
+ entries?: unknown[];
809
+ totalCount?: number;
810
+ page?: number;
811
+ pageSize?: number;
812
+ truncated?: boolean;
813
+ } | null;
814
+
815
+ if (!materialized) {
816
+ throw new CrmCliError(`CRM object "${objectName}" not found`);
817
+ }
818
+ const entries = Array.isArray(materialized.entries)
819
+ ? materialized.entries
820
+ : [];
821
+ if (!withMeta) {
822
+ out(ctx, entries);
823
+ return;
824
+ }
825
+ out(ctx, {
826
+ objectName,
827
+ viewName: readViewName(view) ?? viewName,
828
+ view: summarizeSavedView(view),
829
+ totalCount: materialized.totalCount ?? entries.length,
830
+ returnedCount: entries.length,
831
+ page: materialized.page ?? page,
832
+ pageSize: materialized.pageSize ?? pageSize,
833
+ truncated: materialized.truncated ?? false,
834
+ entries,
835
+ });
836
+ return;
837
+ }
838
+ default:
839
+ throw new CrmCliError(`Unknown crm views verb: ${verb ?? "<none>"}`);
840
+ }
841
+ }
842
+
639
843
  async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
640
844
  const verb = ctx.args.shift();
641
845
  switch (verb) {
@@ -2418,6 +2622,11 @@ Objects (CRM tables):
2418
2622
  dench crm objects rename <from> <to>
2419
2623
  dench crm objects delete <name>
2420
2624
 
2625
+ Views (saved CRM subsets):
2626
+ dench crm views list <object> [--json]
2627
+ dench crm views get <object> <view-name> [--json]
2628
+ dench crm views entries <object> <view-name> [--limit N] [--page N] [--with-meta] [--json]
2629
+
2421
2630
  Fields (columns):
2422
2631
  dench crm fields list <object> [--json]
2423
2632
  dench crm fields create <object> <name> --type text|email|phone|url|number|boolean|date|enum|relation|user|tags|action|file|richtext \\
package/dench.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  resolveHostFromArgs,
28
28
  STAGING_HOST,
29
29
  } from "./host";
30
+ import { resolveOrganizationBySelector } from "./lib/organizationSelector";
30
31
  import { formatApprovalOpenMessage, openUrl } from "./openUrl";
31
32
  import {
32
33
  explicitSessionKeyInput,
@@ -94,6 +95,10 @@ type ConfigFile = {
94
95
  sessions?: Record<string, StoredSession>;
95
96
  };
96
97
 
98
+ function createCliConvexClient(convexUrl: string): ConvexHttpClient {
99
+ return new ConvexHttpClient(convexUrl, { logger: false });
100
+ }
101
+
97
102
  type WorkspaceOverview = {
98
103
  agents: Array<{ _id: string } & JsonRecord>;
99
104
  projects: JsonRecord[];
@@ -223,6 +228,7 @@ const BOOLEAN_FLAGS = new Set([
223
228
  "--compact",
224
229
  "--dev",
225
230
  "--follow",
231
+ "--full-page",
226
232
  "--help",
227
233
  "--mine",
228
234
  "--self",
@@ -470,6 +476,29 @@ Live web search (Exa via the Dench Gateway, auths with the active \`dench signin
470
476
  dench search answer "<query>" [--json]
471
477
  Help: dench search help
472
478
 
479
+ Cloud browser (Browserbase, runs entirely in Dench Cloud):
480
+ dench browser open [url] [--json]
481
+ dench browser inspect [--json]
482
+ dench browser navigate <url>
483
+ dench browser click --selector <css> | --text "Button text"
484
+ dench browser fill --label "Email" --value "me@example.com"
485
+ dench browser screenshot [--output page.jpg] [--full-page]
486
+ dench browser stop
487
+ Help: dench browser help
488
+
489
+ LinkedIn outreach (Browserbase, verified before CRM update):
490
+ dench linkedin connect <profileUrl> [--entry people:<entryId>] [--json]
491
+ Help: dench linkedin help
492
+
493
+ Managed email campaigns (Dench Emailing Service):
494
+ dench email identity verify --from founder@example.com [--json]
495
+ dench email template create --name "Intro" --subject "Hi {{firstName}}" --html-file body.html [--json]
496
+ dench email campaign create --name "Q2 outreach" --identity <id> --template <id> [--json]
497
+ dench email campaign recipients add --campaign <id> --file recipients.jsonl [--json]
498
+ dench email campaign submit --campaign <id> [--json]
499
+ dench email campaign status --campaign <id> [--json]
500
+ Help: dench email help
501
+
473
502
  Image generation / editing (via the Dench Gateway):
474
503
  dench image generate "<prompt>" [--size 1024x1024] [--quality auto|low|medium|high] [--format png|jpeg|webp] [--save-path /workspace/path/file.png] [--output <local file>] [--no-write] [--json]
475
504
  dench image edit "<prompt>" --input <path> [--input <path> ...] [--mask <path>] [--size ...] [--quality ...] [--format ...] [--save-path ...] [--output ...] [--json]
@@ -499,6 +528,7 @@ Spawn a new chat-turn workflow (same as the web UI):
499
528
  File sync:
500
529
  dench fs status [--json] [--workspace /workspace] [--no-hash] [--drift-limit N]
501
530
  dench fs sync --initial --org-id <orgId> [--workspace /workspace]
531
+ dench fs pull <path> [<path>...] [--wait] [--timeout-ms N] [--json]
502
532
  dench fs stream-open <path>
503
533
  dench fs stream-append <token> <chunk>
504
534
  dench fs stream-close <token>
@@ -531,8 +561,6 @@ Self-updating agent harness:
531
561
  dench user show | edit USER.md (per-member)
532
562
  dench tools show | edit TOOLS.md notes section
533
563
  dench mem show | append "<text>" | set MEMORY.md aggregate
534
- dench heartbeat status | enable | disable
535
- | interval <30m|1h|…> | instructions
536
564
  dench bootstrap status | complete | reopen | template
537
565
  dench model default <id|clear>
538
566
  dench model thread <threadId> <id|clear>
@@ -1585,7 +1613,7 @@ async function signin() {
1585
1613
  const explicitKey = explicitSessionKeyInput({ args: filteredArgs });
1586
1614
  const host = resolveHostFromArgs(filteredArgs, undefined);
1587
1615
  const convexUrl = await discoverConvexUrl(host);
1588
- const client = new ConvexHttpClient(convexUrl);
1616
+ const client = createCliConvexClient(convexUrl);
1589
1617
 
1590
1618
  const sessionToken = `dch_agent_${randomUrlSafe(32)}`;
1591
1619
  const sessionTokenHash = await sha256Hex(sessionToken);
@@ -1970,11 +1998,9 @@ async function resolveOrgChoiceForOtp(input: {
1970
1998
  orgSelector: string | null;
1971
1999
  }): Promise<{ organizationId?: string; newOrganizationName?: string }> {
1972
2000
  if (input.orgSelector) {
1973
- const match = input.organizations.find(
1974
- (org) =>
1975
- org.id === input.orgSelector ||
1976
- org.slug === input.orgSelector ||
1977
- org.name === input.orgSelector,
2001
+ const match = resolveOrganizationBySelector(
2002
+ input.organizations,
2003
+ input.orgSelector,
1978
2004
  );
1979
2005
  if (!match) {
1980
2006
  throw new CliError(
@@ -2325,7 +2351,7 @@ async function getRuntime() {
2325
2351
  return {
2326
2352
  mode: "session" as const,
2327
2353
  host: resolveApiHost(host),
2328
- client: new ConvexHttpClient(stored.session.convexUrl),
2354
+ client: createCliConvexClient(stored.session.convexUrl),
2329
2355
  sessionToken: stored.session.sessionToken,
2330
2356
  organization: stored.session.organization,
2331
2357
  };
@@ -2340,48 +2366,55 @@ async function getRuntime() {
2340
2366
  });
2341
2367
  }
2342
2368
 
2343
- // Sandbox session-mode path: workflow minting provisions a real
2344
- // dch_agent_* token at sandbox-create time. Prefer it over apiKey so
2345
- // agentWorkspace.* commands (Composio tools) work inside sandboxes.
2346
- const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
2347
2369
  const sandboxConvexUrl = resolveApiKeyConvexUrl();
2348
- if (sandboxAgentToken && sandboxConvexUrl) {
2349
- const orgId = process.env.DENCH_ORG_ID?.trim();
2350
- const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
2370
+ const orgId = process.env.DENCH_ORG_ID?.trim();
2371
+ const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
2372
+ const sandboxOrganization =
2373
+ orgId && orgSlug
2374
+ ? { id: orgId, name: orgSlug, slug: orgSlug }
2375
+ : undefined;
2376
+
2377
+ // Sandbox / automation path. PREFER the unified Dench API key over the
2378
+ // `dch_agent_*` agent-session token.
2379
+ //
2380
+ // Both are baked into sandbox envVars (src/lib/secrets/sandbox-tokens.ts)
2381
+ // and the server's `requireCrmAccess` / `requireCliWorkspaceAccess`
2382
+ // accept EITHER for the full CRM + files + integration surface. But the
2383
+ // agent-session token EXPIRES (SANDBOX_AGENT_SESSION_TTL_MS, 7d) while
2384
+ // the API key is org-secret-keyed with no TTL. The old order tried the
2385
+ // agent token first and — critically — `getRuntime` keys the session
2386
+ // branch purely on the token being PRESENT, never on validity, so an
2387
+ // expired token in a long-lived / reused sandbox would be used blindly
2388
+ // and the server would reject it, even though a perfectly valid API key
2389
+ // sat right next to it. The server itself documents the API key as the
2390
+ // "preferred path for dench-cli and any automation outside the browser"
2391
+ // and the agent token as legacy back-compat that "new sandboxes do not
2392
+ // need". So prefer the key; it removes the expired-token failure mode
2393
+ // entirely for unattended cron/heartbeat runs.
2394
+ const apiKey = process.env.DENCH_API_KEY?.trim();
2395
+ if (apiKey && sandboxConvexUrl) {
2351
2396
  const apiHost = resolveApiHost(host);
2352
2397
  return {
2353
- mode: "session" as const,
2398
+ mode: "apiKey" as const,
2354
2399
  host: apiHost,
2355
- client: new ConvexHttpClient(sandboxConvexUrl),
2356
- sessionToken: sandboxAgentToken,
2357
- organization:
2358
- orgId && orgSlug
2359
- ? { id: orgId, name: orgSlug, slug: orgSlug }
2360
- : undefined,
2400
+ client: createCliConvexClient(sandboxConvexUrl),
2401
+ sessionToken: apiKey,
2402
+ organization: sandboxOrganization,
2361
2403
  };
2362
2404
  }
2363
2405
 
2364
- // Sandbox / automation path: the unified Dench API key + Convex URL
2365
- // baked into sandbox envVars (see
2366
- // src/lib/secrets/sandbox-tokens.ts) is enough to drive every
2367
- // command that goes through `requireCrmAccess` server-side. We only
2368
- // fall into this branch when no stored session is present, so
2369
- // explicit `dench signin` setups keep their existing behavior.
2370
- const apiKey = process.env.DENCH_API_KEY?.trim();
2371
- const convexUrl = resolveApiKeyConvexUrl();
2372
- if (apiKey && convexUrl) {
2373
- const orgId = process.env.DENCH_ORG_ID?.trim();
2374
- const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
2406
+ // Fallback: no API key present (older sandbox images / custom setups)
2407
+ // use the agent-session token. Still subject to expiry, but it's the
2408
+ // only credential available in this branch.
2409
+ const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
2410
+ if (sandboxAgentToken && sandboxConvexUrl) {
2375
2411
  const apiHost = resolveApiHost(host);
2376
2412
  return {
2377
- mode: "apiKey" as const,
2413
+ mode: "session" as const,
2378
2414
  host: apiHost,
2379
- client: new ConvexHttpClient(convexUrl),
2380
- sessionToken: apiKey,
2381
- organization:
2382
- orgId && orgSlug
2383
- ? { id: orgId, name: orgSlug, slug: orgSlug }
2384
- : undefined,
2415
+ client: createCliConvexClient(sandboxConvexUrl),
2416
+ sessionToken: sandboxAgentToken,
2417
+ organization: sandboxOrganization,
2385
2418
  };
2386
2419
  }
2387
2420
 
@@ -2394,7 +2427,7 @@ async function getRuntime() {
2394
2427
 
2395
2428
  return {
2396
2429
  mode: "dev" as const,
2397
- client: new ConvexHttpClient(readEnv("NEXT_PUBLIC_CONVEX_URL")),
2430
+ client: createCliConvexClient(readEnv("NEXT_PUBLIC_CONVEX_URL")),
2398
2431
  workspaceArgs: {
2399
2432
  devKey: readEnv("DENCH_DEV_AGENT_KEY"),
2400
2433
  workspaceSlug: readEnv("DENCH_WORKSPACE"),
@@ -2933,7 +2966,7 @@ async function listArtifacts(runtime: Runtime, limit?: number) {
2933
2966
  }
2934
2967
 
2935
2968
  async function listSuggestedWork(runtime: Runtime) {
2936
- if (runtime.mode === "session") {
2969
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
2937
2970
  const context = await buildContext(runtime);
2938
2971
  return asRecordArray(asRecord(context)?.suggestedWork);
2939
2972
  }
@@ -3616,6 +3649,44 @@ async function main() {
3616
3649
  return;
3617
3650
  }
3618
3651
 
3652
+ if (
3653
+ command === "browser" &&
3654
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
3655
+ ) {
3656
+ const { runBrowserCommand } = await import("./browser");
3657
+ await runBrowserCommand({
3658
+ args: subcommand ? [subcommand] : ["help"],
3659
+ runtime: { host: "", bearerToken: "" },
3660
+ });
3661
+ return;
3662
+ }
3663
+
3664
+ if (
3665
+ command === "linkedin" &&
3666
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
3667
+ ) {
3668
+ const { runLinkedInCommand } = await import("./linkedin");
3669
+ await runLinkedInCommand({
3670
+ args: subcommand ? [subcommand] : ["help"],
3671
+ runtime: { host: "", bearerToken: "" },
3672
+ });
3673
+ return;
3674
+ }
3675
+
3676
+ if (
3677
+ command === "email" &&
3678
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
3679
+ ) {
3680
+ const { runEmailCommand } = await import("./email");
3681
+ await runEmailCommand({
3682
+ // biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
3683
+ convex: {} as any,
3684
+ args: subcommand ? [subcommand] : ["help"],
3685
+ jsonOutput: json,
3686
+ });
3687
+ return;
3688
+ }
3689
+
3619
3690
  if (
3620
3691
  command === "billing" &&
3621
3692
  (!subcommand || subcommand === "help" || hasFlag("--help"))
@@ -3847,6 +3918,31 @@ async function main() {
3847
3918
  return;
3848
3919
  }
3849
3920
 
3921
+ if (command === "email") {
3922
+ const subArgs = args.slice(args.indexOf("email") + 1);
3923
+ const { runEmailCommand } = await import("./email");
3924
+ const runtime = await getRuntime();
3925
+ const authedRuntime = requireAuthenticatedRuntime(runtime, "dench email");
3926
+ let gatewayAuth:
3927
+ | Awaited<ReturnType<typeof resolveGatewayCommandAuth>>
3928
+ | undefined;
3929
+ try {
3930
+ gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench email");
3931
+ } catch {
3932
+ gatewayAuth = undefined;
3933
+ }
3934
+ await runEmailCommand({
3935
+ convex: authedRuntime.client,
3936
+ args: subArgs.filter((arg) => arg !== "--json"),
3937
+ jsonOutput: json,
3938
+ sessionToken: authedRuntime.sessionToken,
3939
+ bearerToken: gatewayAuth?.bearerToken ?? authedRuntime.sessionToken,
3940
+ gatewayBaseUrl: gatewayAuth?.gatewayBaseUrl,
3941
+ gatewayHeaders: gatewayAuth?.gatewayHeaders,
3942
+ });
3943
+ return;
3944
+ }
3945
+
3850
3946
  if (command === "apps") {
3851
3947
  const runtime = await getRuntime();
3852
3948
  const gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench apps");
@@ -3880,6 +3976,39 @@ async function main() {
3880
3976
  return;
3881
3977
  }
3882
3978
 
3979
+ if (command === "browser") {
3980
+ const subArgs = args.slice(args.indexOf("browser") + 1);
3981
+ const { runBrowserCommand } = await import("./browser");
3982
+ const runtime = await getRuntime();
3983
+ const authedRuntime = requireAuthenticatedRuntime(runtime, "dench browser");
3984
+ await runBrowserCommand({
3985
+ args: subArgs,
3986
+ runtime: {
3987
+ host: authedRuntime.host,
3988
+ bearerToken: authedRuntime.sessionToken,
3989
+ },
3990
+ });
3991
+ return;
3992
+ }
3993
+
3994
+ if (command === "linkedin") {
3995
+ const subArgs = args.slice(args.indexOf("linkedin") + 1);
3996
+ const { runLinkedInCommand } = await import("./linkedin");
3997
+ const runtime = await getRuntime();
3998
+ const authedRuntime = requireAuthenticatedRuntime(
3999
+ runtime,
4000
+ "dench linkedin",
4001
+ );
4002
+ await runLinkedInCommand({
4003
+ args: subArgs,
4004
+ runtime: {
4005
+ host: authedRuntime.host,
4006
+ bearerToken: authedRuntime.sessionToken,
4007
+ },
4008
+ });
4009
+ return;
4010
+ }
4011
+
3883
4012
  if (command === "chat") {
3884
4013
  const subArgs = args.slice(args.indexOf("chat") + 1);
3885
4014
  const sub = subArgs[0];
@@ -4010,7 +4139,7 @@ async function main() {
4010
4139
  // ── OpenClaw-parity self-update CLI surface ──────────────────────────
4011
4140
  //
4012
4141
  // Dispatch all `dench identity / organisation / user / tools / mem /
4013
- // heartbeat / bootstrap / model / daily` traffic through the shared
4142
+ // bootstrap / model / daily` traffic through the shared
4014
4143
  // agent-config CLI module. Each command requires the same auth shape
4015
4144
  // as `dench cron` (Bearer DENCH_API_KEY or `dench signin` session
4016
4145
  // token) so server-side `requireCronAccess` accepts both.
@@ -4020,7 +4149,6 @@ async function main() {
4020
4149
  "user",
4021
4150
  "tools",
4022
4151
  "mem",
4023
- "heartbeat",
4024
4152
  "bootstrap",
4025
4153
  "model",
4026
4154
  "daily",
@@ -4068,10 +4196,6 @@ async function main() {
4068
4196
  await mod.runMemoryAggregateCommand(ctxBase);
4069
4197
  return;
4070
4198
  }
4071
- if (command === "heartbeat") {
4072
- await mod.runHeartbeatCommand(ctxBase);
4073
- return;
4074
- }
4075
4199
  if (command === "bootstrap") {
4076
4200
  await mod.runBootstrapCommand(ctxBase);
4077
4201
  return;