@dench.com/cli 2.1.1 → 2.1.2

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,20 @@ 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
+
473
493
  Image generation / editing (via the Dench Gateway):
474
494
  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
495
  dench image edit "<prompt>" --input <path> [--input <path> ...] [--mask <path>] [--size ...] [--quality ...] [--format ...] [--save-path ...] [--output ...] [--json]
@@ -499,6 +519,7 @@ Spawn a new chat-turn workflow (same as the web UI):
499
519
  File sync:
500
520
  dench fs status [--json] [--workspace /workspace] [--no-hash] [--drift-limit N]
501
521
  dench fs sync --initial --org-id <orgId> [--workspace /workspace]
522
+ dench fs pull <path> [<path>...] [--wait] [--timeout-ms N] [--json]
502
523
  dench fs stream-open <path>
503
524
  dench fs stream-append <token> <chunk>
504
525
  dench fs stream-close <token>
@@ -1585,7 +1606,7 @@ async function signin() {
1585
1606
  const explicitKey = explicitSessionKeyInput({ args: filteredArgs });
1586
1607
  const host = resolveHostFromArgs(filteredArgs, undefined);
1587
1608
  const convexUrl = await discoverConvexUrl(host);
1588
- const client = new ConvexHttpClient(convexUrl);
1609
+ const client = createCliConvexClient(convexUrl);
1589
1610
 
1590
1611
  const sessionToken = `dch_agent_${randomUrlSafe(32)}`;
1591
1612
  const sessionTokenHash = await sha256Hex(sessionToken);
@@ -1970,11 +1991,9 @@ async function resolveOrgChoiceForOtp(input: {
1970
1991
  orgSelector: string | null;
1971
1992
  }): Promise<{ organizationId?: string; newOrganizationName?: string }> {
1972
1993
  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,
1994
+ const match = resolveOrganizationBySelector(
1995
+ input.organizations,
1996
+ input.orgSelector,
1978
1997
  );
1979
1998
  if (!match) {
1980
1999
  throw new CliError(
@@ -2325,7 +2344,7 @@ async function getRuntime() {
2325
2344
  return {
2326
2345
  mode: "session" as const,
2327
2346
  host: resolveApiHost(host),
2328
- client: new ConvexHttpClient(stored.session.convexUrl),
2347
+ client: createCliConvexClient(stored.session.convexUrl),
2329
2348
  sessionToken: stored.session.sessionToken,
2330
2349
  organization: stored.session.organization,
2331
2350
  };
@@ -2340,48 +2359,55 @@ async function getRuntime() {
2340
2359
  });
2341
2360
  }
2342
2361
 
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
2362
  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();
2363
+ const orgId = process.env.DENCH_ORG_ID?.trim();
2364
+ const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
2365
+ const sandboxOrganization =
2366
+ orgId && orgSlug
2367
+ ? { id: orgId, name: orgSlug, slug: orgSlug }
2368
+ : undefined;
2369
+
2370
+ // Sandbox / automation path. PREFER the unified Dench API key over the
2371
+ // `dch_agent_*` agent-session token.
2372
+ //
2373
+ // Both are baked into sandbox envVars (src/lib/secrets/sandbox-tokens.ts)
2374
+ // and the server's `requireCrmAccess` / `requireCliWorkspaceAccess`
2375
+ // accept EITHER for the full CRM + files + integration surface. But the
2376
+ // agent-session token EXPIRES (SANDBOX_AGENT_SESSION_TTL_MS, 7d) while
2377
+ // the API key is org-secret-keyed with no TTL. The old order tried the
2378
+ // agent token first and — critically — `getRuntime` keys the session
2379
+ // branch purely on the token being PRESENT, never on validity, so an
2380
+ // expired token in a long-lived / reused sandbox would be used blindly
2381
+ // and the server would reject it, even though a perfectly valid API key
2382
+ // sat right next to it. The server itself documents the API key as the
2383
+ // "preferred path for dench-cli and any automation outside the browser"
2384
+ // and the agent token as legacy back-compat that "new sandboxes do not
2385
+ // need". So prefer the key; it removes the expired-token failure mode
2386
+ // entirely for unattended cron/heartbeat runs.
2387
+ const apiKey = process.env.DENCH_API_KEY?.trim();
2388
+ if (apiKey && sandboxConvexUrl) {
2351
2389
  const apiHost = resolveApiHost(host);
2352
2390
  return {
2353
- mode: "session" as const,
2391
+ mode: "apiKey" as const,
2354
2392
  host: apiHost,
2355
- client: new ConvexHttpClient(sandboxConvexUrl),
2356
- sessionToken: sandboxAgentToken,
2357
- organization:
2358
- orgId && orgSlug
2359
- ? { id: orgId, name: orgSlug, slug: orgSlug }
2360
- : undefined,
2393
+ client: createCliConvexClient(sandboxConvexUrl),
2394
+ sessionToken: apiKey,
2395
+ organization: sandboxOrganization,
2361
2396
  };
2362
2397
  }
2363
2398
 
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();
2399
+ // Fallback: no API key present (older sandbox images / custom setups)
2400
+ // use the agent-session token. Still subject to expiry, but it's the
2401
+ // only credential available in this branch.
2402
+ const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
2403
+ if (sandboxAgentToken && sandboxConvexUrl) {
2375
2404
  const apiHost = resolveApiHost(host);
2376
2405
  return {
2377
- mode: "apiKey" as const,
2406
+ mode: "session" as const,
2378
2407
  host: apiHost,
2379
- client: new ConvexHttpClient(convexUrl),
2380
- sessionToken: apiKey,
2381
- organization:
2382
- orgId && orgSlug
2383
- ? { id: orgId, name: orgSlug, slug: orgSlug }
2384
- : undefined,
2408
+ client: createCliConvexClient(sandboxConvexUrl),
2409
+ sessionToken: sandboxAgentToken,
2410
+ organization: sandboxOrganization,
2385
2411
  };
2386
2412
  }
2387
2413
 
@@ -2394,7 +2420,7 @@ async function getRuntime() {
2394
2420
 
2395
2421
  return {
2396
2422
  mode: "dev" as const,
2397
- client: new ConvexHttpClient(readEnv("NEXT_PUBLIC_CONVEX_URL")),
2423
+ client: createCliConvexClient(readEnv("NEXT_PUBLIC_CONVEX_URL")),
2398
2424
  workspaceArgs: {
2399
2425
  devKey: readEnv("DENCH_DEV_AGENT_KEY"),
2400
2426
  workspaceSlug: readEnv("DENCH_WORKSPACE"),
@@ -2933,7 +2959,7 @@ async function listArtifacts(runtime: Runtime, limit?: number) {
2933
2959
  }
2934
2960
 
2935
2961
  async function listSuggestedWork(runtime: Runtime) {
2936
- if (runtime.mode === "session") {
2962
+ if (runtime.mode === "session" || runtime.mode === "apiKey") {
2937
2963
  const context = await buildContext(runtime);
2938
2964
  return asRecordArray(asRecord(context)?.suggestedWork);
2939
2965
  }
@@ -3616,6 +3642,30 @@ async function main() {
3616
3642
  return;
3617
3643
  }
3618
3644
 
3645
+ if (
3646
+ command === "browser" &&
3647
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
3648
+ ) {
3649
+ const { runBrowserCommand } = await import("./browser");
3650
+ await runBrowserCommand({
3651
+ args: subcommand ? [subcommand] : ["help"],
3652
+ runtime: { host: "", bearerToken: "" },
3653
+ });
3654
+ return;
3655
+ }
3656
+
3657
+ if (
3658
+ command === "linkedin" &&
3659
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
3660
+ ) {
3661
+ const { runLinkedInCommand } = await import("./linkedin");
3662
+ await runLinkedInCommand({
3663
+ args: subcommand ? [subcommand] : ["help"],
3664
+ runtime: { host: "", bearerToken: "" },
3665
+ });
3666
+ return;
3667
+ }
3668
+
3619
3669
  if (
3620
3670
  command === "billing" &&
3621
3671
  (!subcommand || subcommand === "help" || hasFlag("--help"))
@@ -3880,6 +3930,39 @@ async function main() {
3880
3930
  return;
3881
3931
  }
3882
3932
 
3933
+ if (command === "browser") {
3934
+ const subArgs = args.slice(args.indexOf("browser") + 1);
3935
+ const { runBrowserCommand } = await import("./browser");
3936
+ const runtime = await getRuntime();
3937
+ const authedRuntime = requireAuthenticatedRuntime(runtime, "dench browser");
3938
+ await runBrowserCommand({
3939
+ args: subArgs,
3940
+ runtime: {
3941
+ host: authedRuntime.host,
3942
+ bearerToken: authedRuntime.sessionToken,
3943
+ },
3944
+ });
3945
+ return;
3946
+ }
3947
+
3948
+ if (command === "linkedin") {
3949
+ const subArgs = args.slice(args.indexOf("linkedin") + 1);
3950
+ const { runLinkedInCommand } = await import("./linkedin");
3951
+ const runtime = await getRuntime();
3952
+ const authedRuntime = requireAuthenticatedRuntime(
3953
+ runtime,
3954
+ "dench linkedin",
3955
+ );
3956
+ await runLinkedInCommand({
3957
+ args: subArgs,
3958
+ runtime: {
3959
+ host: authedRuntime.host,
3960
+ bearerToken: authedRuntime.sessionToken,
3961
+ },
3962
+ });
3963
+ return;
3964
+ }
3965
+
3883
3966
  if (command === "chat") {
3884
3967
  const subArgs = args.slice(args.indexOf("chat") + 1);
3885
3968
  const sub = subArgs[0];