@dench.com/cli 3.8.5-staging.406 → 3.9.0-staging.415

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/campaign.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  linesTemplateFromFile,
29
29
  normalizedLineKey,
30
30
  } from "./lib/campaign-lines";
31
+ import { campaignReferenceResult } from "./lib/campaign-reference";
31
32
  import {
32
33
  checkCampaignSteps,
33
34
  hasHtmlAndTextBody,
@@ -56,6 +57,11 @@ import {
56
57
  } from "./lib/cli-args";
57
58
  import { cliVersion } from "./lib/cli-version";
58
59
  import { formatCents } from "./lib/search-pricing";
60
+ import {
61
+ boundWorkspaceReferences,
62
+ type WorkspaceReferences,
63
+ withWorkspaceReferenceEnvelopeUrls,
64
+ } from "./lib/workspace-references";
59
65
 
60
66
  export class CampaignCliError extends Error {}
61
67
 
@@ -74,6 +80,7 @@ type CampaignCliContext = {
74
80
  jsonOutput: boolean;
75
81
  sessionToken?: string;
76
82
  host?: string;
83
+ workspaceSlug?: string;
77
84
  };
78
85
 
79
86
  const OUTREACH = "functions/outreachSequences";
@@ -1382,12 +1389,43 @@ async function loadDetail(
1382
1389
 
1383
1390
  async function runList(ctx: CampaignCliContext): Promise<void> {
1384
1391
  const includeArchived = hasFlag(ctx.args, "--all");
1392
+ const withReferences = hasFlag(ctx.args, "--with-references");
1385
1393
  unknownFlags(ctx.args, "campaign list");
1386
- const rows = await listCampaigns(ctx, includeArchived);
1394
+ const response = withReferences
1395
+ ? await callQuery(ctx, `${OUTREACH}:listSequencesWithReferences`, {
1396
+ includeArchived,
1397
+ })
1398
+ : await listCampaigns(ctx, includeArchived);
1399
+ const envelope = response as {
1400
+ data?: CampaignRow[];
1401
+ } & Partial<WorkspaceReferences>;
1402
+ const rows = Array.isArray(response)
1403
+ ? (response as CampaignRow[])
1404
+ : (envelope.data ?? []);
1405
+ const payload = withReferences
1406
+ ? withWorkspaceReferenceEnvelopeUrls(
1407
+ {
1408
+ data: rows,
1409
+ ...(envelope.references
1410
+ ? {
1411
+ references: envelope.references,
1412
+ referenceCoverage: envelope.referenceCoverage,
1413
+ }
1414
+ : boundWorkspaceReferences(
1415
+ rows.flatMap(
1416
+ (row) =>
1417
+ campaignReferenceResult(ctx, {}, String(row._id), row.name)
1418
+ .references,
1419
+ ),
1420
+ )),
1421
+ },
1422
+ ctx,
1423
+ )
1424
+ : rows;
1387
1425
  if (rows.length === 0) {
1388
1426
  out(
1389
1427
  ctx,
1390
- rows,
1428
+ payload,
1391
1429
  includeArchived
1392
1430
  ? 'No campaigns yet. Create one with: dench campaign create --name "..."'
1393
1431
  : 'No live campaigns. Add --all to include archived ones, or create one with: dench campaign create --name "..."',
@@ -1401,7 +1439,7 @@ async function runList(ctx: CampaignCliContext): Promise<void> {
1401
1439
  const owner = row.ownerName ? ` · ${row.ownerName}` : "";
1402
1440
  return `${status} ${row.name} — ${people}, ${replied}${owner} [${row._id}]`;
1403
1441
  });
1404
- out(ctx, rows, lines);
1442
+ out(ctx, payload, lines);
1405
1443
  }
1406
1444
 
1407
1445
  async function runCreate(ctx: CampaignCliContext): Promise<void> {
@@ -1420,7 +1458,7 @@ async function runCreate(ctx: CampaignCliContext): Promise<void> {
1420
1458
  name,
1421
1459
  timezone,
1422
1460
  ...senderPools,
1423
- })) as { sequenceId: string };
1461
+ })) as { sequenceId: string; references?: unknown[] };
1424
1462
  if (hasScheduleChanges(patch)) {
1425
1463
  await callMutation(ctx, `${OUTREACH}:updateDraft`, {
1426
1464
  sequenceId: created.sequenceId,
@@ -1444,18 +1482,27 @@ async function runCreate(ctx: CampaignCliContext): Promise<void> {
1444
1482
  ...(patch.startsAt !== undefined ? { startsAt: patch.startsAt } : {}),
1445
1483
  ...(patch.endsAt !== undefined ? { endsAt: patch.endsAt } : {}),
1446
1484
  };
1447
- out(ctx, payload, [
1448
- `Created draft campaign "${name}" [${created.sequenceId}]`,
1449
- `Send window: ${schedules
1450
- .map(
1451
- (s) =>
1452
- `${formatWeekdays(s.weekdays)} ${s.startHour}-${s.endHour} ${s.timezone}`,
1453
- )
1454
- .join("; ")}`,
1455
- "Next: dench campaign steps set <campaign> --file steps.json",
1456
- 'Add people: dench campaign search --titles Founder --limit 25 --out audience.json, then dench campaign add <campaign> --people-file audience.json (or --csv, --list, --object lead --view "<view>")',
1457
- "Then: dench campaign launch <campaign> --check",
1458
- ]);
1485
+ out(
1486
+ ctx,
1487
+ campaignReferenceResult(
1488
+ ctx,
1489
+ { ...payload, references: created.references },
1490
+ created.sequenceId,
1491
+ name,
1492
+ ),
1493
+ [
1494
+ `Created draft campaign "${name}" [${created.sequenceId}]`,
1495
+ `Send window: ${schedules
1496
+ .map(
1497
+ (s) =>
1498
+ `${formatWeekdays(s.weekdays)} ${s.startHour}-${s.endHour} ${s.timezone}`,
1499
+ )
1500
+ .join("; ")}`,
1501
+ "Next: dench campaign steps set <campaign> --file steps.json",
1502
+ 'Add people: dench campaign search --titles Founder --limit 25 --out audience.json, then dench campaign add <campaign> --people-file audience.json (or --csv, --list, --object lead --view "<view>")',
1503
+ "Then: dench campaign launch <campaign> --check",
1504
+ ],
1505
+ );
1459
1506
  }
1460
1507
 
1461
1508
  async function runDuplicate(ctx: CampaignCliContext): Promise<void> {
@@ -1468,7 +1515,7 @@ async function runDuplicate(ctx: CampaignCliContext): Promise<void> {
1468
1515
  const row = await resolveCampaign(ctx, reference);
1469
1516
  const created = (await callMutation(ctx, `${OUTREACH}:duplicateSequence`, {
1470
1517
  sequenceId: row._id,
1471
- })) as { sequenceId: string };
1518
+ })) as { sequenceId: string; references?: unknown[] };
1472
1519
  if (name !== undefined) {
1473
1520
  await callMutation(ctx, `${OUTREACH}:updateDraft`, {
1474
1521
  sequenceId: created.sequenceId,
@@ -1479,14 +1526,20 @@ async function runDuplicate(ctx: CampaignCliContext): Promise<void> {
1479
1526
  const steps = row.stepCount ?? 0;
1480
1527
  out(
1481
1528
  ctx,
1482
- {
1483
- sequenceId: created.sequenceId,
1484
- name: copyName,
1485
- status: "draft",
1486
- duplicatedFrom: row._id,
1487
- stepCount: steps,
1488
- people: 0,
1489
- },
1529
+ campaignReferenceResult(
1530
+ ctx,
1531
+ {
1532
+ references: created.references,
1533
+ sequenceId: created.sequenceId,
1534
+ name: copyName,
1535
+ status: "draft",
1536
+ duplicatedFrom: row._id,
1537
+ stepCount: steps,
1538
+ people: 0,
1539
+ },
1540
+ created.sequenceId,
1541
+ copyName,
1542
+ ),
1490
1543
  [
1491
1544
  `Duplicated "${row.name}" as the draft "${copyName}" [${created.sequenceId}]: ${steps} ${steps === 1 ? "step" : "steps"}, the same schedule, senders and personalized lines, its lead source (paused), and nobody in it yet.`,
1492
1545
  `Next: dench campaign add ${created.sequenceId} ... (up to ${MAX_CAMPAIGN_PEOPLE} people), then dench campaign launch ${created.sequenceId} --check`,
@@ -1631,7 +1684,19 @@ async function runGet(ctx: CampaignCliContext): Promise<void> {
1631
1684
  }
1632
1685
  out(
1633
1686
  ctx,
1634
- { detail, analytics, live, ...(analyticsError ? { analyticsError } : {}) },
1687
+ campaignReferenceResult(
1688
+ ctx,
1689
+ {
1690
+ detail,
1691
+ analytics,
1692
+ live,
1693
+ references: (detail as CampaignDetail & Partial<WorkspaceReferences>)
1694
+ .references,
1695
+ ...(analyticsError ? { analyticsError } : {}),
1696
+ },
1697
+ sequenceId,
1698
+ sequence.name,
1699
+ ),
1635
1700
  lines,
1636
1701
  );
1637
1702
  }
@@ -4234,7 +4299,7 @@ Start here (references/CAMPAIGNS.md has the full recipe, from a brief to launch)
4234
4299
  6. dench campaign launch <campaign> --check, then launch once the user says yes
4235
4300
 
4236
4301
  Browse:
4237
- dench campaign list [--all] # live campaigns; --all includes archived
4302
+ dench campaign list [--all] [--with-references] # live campaigns; --all includes archived
4238
4303
  dench campaign get <campaign> # status, schedule, funnel, Now/Next, holds, people
4239
4304
  dench campaign people <campaign> [--status active|paused|finished|replied|stopped|bounced|failed|draft|complained|unsubscribed]
4240
4305
  dench campaign events <campaign> [--limit 50] [--max-pages 1] # activity log, newest first
@@ -4468,6 +4533,7 @@ export async function runCampaignCommand(opts: {
4468
4533
  sessionToken?: string;
4469
4534
  jsonOutput?: boolean;
4470
4535
  host?: string;
4536
+ workspaceSlug?: string;
4471
4537
  }): Promise<void> {
4472
4538
  const args = [...opts.args];
4473
4539
  const jsonOutput = hasFlag(args, "--json") || opts.jsonOutput === true;
@@ -4477,6 +4543,7 @@ export async function runCampaignCommand(opts: {
4477
4543
  jsonOutput,
4478
4544
  sessionToken: opts.sessionToken,
4479
4545
  host: opts.host,
4546
+ workspaceSlug: opts.workspaceSlug,
4480
4547
  };
4481
4548
  const subcommand = args.shift();
4482
4549
  if (
package/crm.ts CHANGED
@@ -29,6 +29,7 @@
29
29
  * crm docs list / create / link
30
30
  * crm actions list / run / runs
31
31
  */
32
+
32
33
  import type { ConvexHttpClient } from "convex/browser";
33
34
  import { makeFunctionReference } from "convex/server";
34
35
  import { runAnalyticsCommand } from "./crm-analytics";
@@ -76,6 +77,12 @@ import {
76
77
  } from "./lib/enrichment-gateway";
77
78
  import { closestMatch, levenshteinDistance } from "./lib/levenshtein";
78
79
  import { publicErrorMessage } from "./lib/public-error";
80
+ import {
81
+ boundWorkspaceReferences,
82
+ type WorkspaceReference,
83
+ withWorkspaceReferenceEnvelopeUrls,
84
+ workspaceReferenceHref,
85
+ } from "./lib/workspace-references";
79
86
 
80
87
  type JsonRecord = Record<string, unknown>;
81
88
 
@@ -663,23 +670,38 @@ export async function applyBatchOpsInChunks(
663
670
  idempotencyKey: string | undefined,
664
671
  allowUnknown = false,
665
672
  onConflict?: CrmOnConflictPolicy,
666
- ): Promise<{ chunks: number; results: unknown[] }> {
673
+ withReferences = false,
674
+ ): Promise<{
675
+ chunks: number;
676
+ results: unknown[];
677
+ references: WorkspaceReference[];
678
+ }> {
667
679
  const chunks = chunkArray(ops, CRM_BATCH_CHUNK_SIZE);
668
680
  const results: unknown[] = [];
681
+ const references: WorkspaceReference[] = [];
669
682
  for (const [chunkIndex, chunk] of chunks.entries()) {
670
683
  const chunkKey = chunkIdempotencyKey(idempotencyKey, chunkIndex);
671
684
  const batchResult = (await callMutation(ctx, api.batch.apply, {
672
685
  ops: chunk,
686
+ ...(withReferences ? { withReferences: true } : {}),
673
687
  ...runContextArgs(ctx),
674
688
  ...(chunkKey ? { idempotencyKey: chunkKey } : {}),
675
689
  ...(allowUnknown ? { allowUnknown: true } : {}),
676
690
  ...(onConflict ? { onConflict } : {}),
677
- })) as { results?: unknown[] } | null | undefined;
691
+ })) as
692
+ | { results?: unknown[]; references?: WorkspaceReference[] }
693
+ | null
694
+ | undefined;
695
+ references.push(...(batchResult?.references ?? []));
678
696
  if (Array.isArray(batchResult?.results)) {
679
697
  results.push(...batchResult.results);
680
698
  }
681
699
  }
682
- return { chunks: chunks.length, results };
700
+ return {
701
+ chunks: chunks.length,
702
+ results,
703
+ references: boundWorkspaceReferences(references).references,
704
+ };
683
705
  }
684
706
 
685
707
  function runContextArgs(ctx: CrmCliContext): Record<string, string> {
@@ -871,6 +893,12 @@ export function out(ctx: CrmCliContext, value: unknown): void {
871
893
  console.log(JSON.stringify(value, null, 2));
872
894
  }
873
895
 
896
+ /** Only known product envelopes carry navigation metadata. Cells, documents,
897
+ * exports and arbitrary JSON values must pass through out() unchanged. */
898
+ function outWithReferences(ctx: CrmCliContext, value: unknown): void {
899
+ out(ctx, withWorkspaceReferenceEnvelopeUrls(value, ctx));
900
+ }
901
+
874
902
  function getFieldEnrichmentFlag(args: string[]):
875
903
  | {
876
904
  category: EnrichmentCategory;
@@ -1009,6 +1037,9 @@ const api = {
1009
1037
  },
1010
1038
  entries: {
1011
1039
  list: makeFunctionReference<"query">("functions/crm/entries:list"),
1040
+ listWithReferences: makeFunctionReference<"query">(
1041
+ "functions/crm/entries:listWithReferences",
1042
+ ),
1012
1043
  listPage: makeFunctionReference<"query">("functions/crm/entries:listPage"),
1013
1044
  get: makeFunctionReference<"query">("functions/crm/entries:get"),
1014
1045
  create: makeFunctionReference<"mutation">("functions/crm/entries:create"),
@@ -1029,6 +1060,9 @@ const api = {
1029
1060
  append: makeFunctionReference<"mutation">("functions/crm/cells:append"),
1030
1061
  },
1031
1062
  query: {
1063
+ queryEntriesWithReferences: makeFunctionReference<"action">(
1064
+ "functions/crm/query:runQueryEntriesWithReferences",
1065
+ ),
1032
1066
  queryEntries: makeFunctionReference<"action">(
1033
1067
  "functions/crm/query:runQueryEntries",
1034
1068
  ),
@@ -1037,6 +1071,9 @@ const api = {
1037
1071
  ),
1038
1072
  aggregate: makeFunctionReference<"query">("functions/crm/query:aggregate"),
1039
1073
  search: makeFunctionReference<"query">("functions/crm/query:search"),
1074
+ searchWithReferences: makeFunctionReference<"query">(
1075
+ "functions/crm/query:searchWithReferences",
1076
+ ),
1040
1077
  searchPage: makeFunctionReference<"query">(
1041
1078
  "functions/crm/query:searchPage",
1042
1079
  ),
@@ -1126,7 +1163,7 @@ export async function runCrmCommand(opts: {
1126
1163
  }
1127
1164
  if (isCrmReadHelpRequest(opts.args)) {
1128
1165
  if (subcommand === "query") {
1129
- console.log(`Usage: dench crm query <object> [--where '<JSON>'] [--select Name,Email] [--sort -Score] [--limit N] [--json]
1166
+ console.log(`Usage: dench crm query <object> [--with-references] [--where '<JSON>'] [--select Name,Email] [--sort -Score] [--limit N] [--json]
1130
1167
  --page [--cursor '<continueCursor>'] Return {page,isDone,continueCursor} in index order; omit --sort.
1131
1168
  --assigned-to me|unassigned|<userId|email> (or --mine) [--assignee-field <Field>]
1132
1169
  Default limit: 100. Maximum: 1000 for a one-shot array, 100 for a cursor page.
@@ -1482,26 +1519,60 @@ async function runSidebarCommand(ctx: CrmCliContext): Promise<void> {
1482
1519
  }
1483
1520
  }
1484
1521
 
1522
+ function objectResultReferences(value: unknown) {
1523
+ const refs: WorkspaceReference[] = Array.isArray(value)
1524
+ ? value.flatMap((raw) => {
1525
+ if (!raw || typeof raw !== "object" || typeof raw.name !== "string")
1526
+ return [];
1527
+ const identity = {
1528
+ kind: "crm-object" as const,
1529
+ objectName: raw.name,
1530
+ ...(typeof raw._id === "string" ? { objectId: raw._id } : {}),
1531
+ };
1532
+ return [
1533
+ {
1534
+ ...identity,
1535
+ label:
1536
+ typeof raw.displayName === "string" && raw.displayName
1537
+ ? raw.displayName
1538
+ : raw.name,
1539
+ href: workspaceReferenceHref(identity),
1540
+ ...(typeof raw.icon === "string" ? { objectIcon: raw.icon } : {}),
1541
+ },
1542
+ ];
1543
+ })
1544
+ : [];
1545
+ return boundWorkspaceReferences(refs);
1546
+ }
1547
+
1485
1548
  async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
1486
1549
  const verb = ctx.args.shift();
1487
1550
  switch (verb) {
1488
1551
  case "list": {
1489
1552
  const compact = hasFlag(ctx.args, "--compact");
1553
+ const references = hasFlag(ctx.args, "--with-references");
1490
1554
  assertNoUnknownFlags(ctx.args, "crm objects list");
1491
- out(
1555
+ const data = await callQuery(
1492
1556
  ctx,
1493
- await callQuery(
1494
- ctx,
1495
- compact ? api.objects.listCompact : api.objects.list,
1496
- {},
1497
- ),
1557
+ compact ? api.objects.listCompact : api.objects.list,
1558
+ {},
1559
+ );
1560
+ outWithReferences(
1561
+ ctx,
1562
+ references ? { data, ...objectResultReferences(data) } : data,
1498
1563
  );
1499
1564
  return;
1500
1565
  }
1501
1566
  case "get": {
1502
1567
  const name = shift(ctx.args, "object name");
1503
1568
  assertNoUnknownFlags(ctx.args, "crm objects get");
1504
- out(ctx, await callQuery(ctx, api.objects.get, { name }));
1569
+ const data = await callQuery(ctx, api.objects.get, { name });
1570
+ outWithReferences(
1571
+ ctx,
1572
+ data && typeof data === "object"
1573
+ ? { ...data, ...objectResultReferences([data]) }
1574
+ : data,
1575
+ );
1505
1576
  return;
1506
1577
  }
1507
1578
  case "create": {
@@ -1514,7 +1585,7 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
1514
1585
  const defaultView = getFlag(ctx.args, "--default-view") as any;
1515
1586
  const icon = getFlag(ctx.args, "--icon");
1516
1587
  assertNoUnknownFlags(ctx.args, "crm objects create");
1517
- out(
1588
+ outWithReferences(
1518
1589
  ctx,
1519
1590
  await callMutation(ctx, api.objects.create, {
1520
1591
  name,
@@ -2459,6 +2530,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2459
2530
  case "list": {
2460
2531
  const objectName = shift(ctx.args, "object name");
2461
2532
  const withMeta = hasFlag(ctx.args, "--with-meta");
2533
+ const withReferences = hasFlag(ctx.args, "--with-references");
2462
2534
  const cursorSpecified = ctx.args.includes("--cursor");
2463
2535
  const cursorValue = ctx.args[ctx.args.indexOf("--cursor") + 1];
2464
2536
  if (
@@ -2496,13 +2568,17 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2496
2568
  if (!withMeta) {
2497
2569
  // Bare-array output: the clamp warning already went to stderr
2498
2570
  // (stdout must stay a valid JSON array in --json mode).
2499
- out(
2571
+ outWithReferences(
2500
2572
  ctx,
2501
- await callQuery(ctx, api.entries.list, {
2502
- objectName,
2503
- limit: limit.effective,
2504
- ...assigneeScopeArgs,
2505
- }),
2573
+ await callQuery(
2574
+ ctx,
2575
+ withReferences ? api.entries.listWithReferences : api.entries.list,
2576
+ {
2577
+ objectName,
2578
+ limit: limit.effective,
2579
+ ...assigneeScopeArgs,
2580
+ },
2581
+ ),
2506
2582
  );
2507
2583
  return;
2508
2584
  }
@@ -2513,6 +2589,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2513
2589
  ? callQuery(ctx, api.query.scanEntries, {
2514
2590
  objectName,
2515
2591
  dsl: { limit: limit.effective },
2592
+ withReferences: true,
2516
2593
  paginationOpts: {
2517
2594
  cursor: cursor ?? null,
2518
2595
  numItems: limit.effective,
@@ -2522,12 +2599,16 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2522
2599
  : callQuery(ctx, api.entries.listPage, {
2523
2600
  objectName,
2524
2601
  limit: limit.effective,
2602
+ withReferences: true,
2525
2603
  ...(cursor !== undefined ? { cursor } : {}),
2526
2604
  }),
2527
2605
  callQuery(ctx, api.objects.get, { name: objectName }),
2528
2606
  ])) as [
2529
2607
  {
2530
2608
  page?: unknown[];
2609
+ objectName?: string;
2610
+ references?: WorkspaceReference[];
2611
+ referenceCoverage?: { available: number };
2531
2612
  isDone?: boolean;
2532
2613
  continueCursor?: string | null;
2533
2614
  truncated?: boolean;
@@ -2544,8 +2625,14 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2544
2625
  : isDone === true && !truncated
2545
2626
  ? false
2546
2627
  : null;
2547
- out(ctx, {
2548
- objectName,
2628
+ outWithReferences(ctx, {
2629
+ ...(result?.references
2630
+ ? boundWorkspaceReferences(
2631
+ result.references,
2632
+ result.referenceCoverage?.available,
2633
+ )
2634
+ : {}),
2635
+ objectName: result?.objectName ?? objectName,
2549
2636
  totalCount:
2550
2637
  typeof object?.entryCount === "number" ? object.entryCount : null,
2551
2638
  totalCountScope: "object",
@@ -2700,7 +2787,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2700
2787
  }),
2701
2788
  );
2702
2789
  if (!withContext) {
2703
- out(ctx, entry);
2790
+ outWithReferences(ctx, entry);
2704
2791
  return;
2705
2792
  }
2706
2793
  const [activity, notes, brief] = await Promise.all([
@@ -2724,7 +2811,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2724
2811
  { objectName, entryId },
2725
2812
  ),
2726
2813
  ]);
2727
- out(ctx, {
2814
+ outWithReferences(ctx, {
2728
2815
  ...(entry as Record<string, unknown>),
2729
2816
  context: {
2730
2817
  brief,
@@ -2770,7 +2857,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2770
2857
  // Single-row writes rely on the server's schema validation (which
2771
2858
  // returns the same "did you mean …" hint the CLI would). No local
2772
2859
  // pre-fetch — that would add a round-trip to every create.
2773
- out(
2860
+ outWithReferences(
2774
2861
  ctx,
2775
2862
  withCrmEntryDeepLinks(
2776
2863
  ctx,
@@ -2823,7 +2910,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2823
2910
  }),
2824
2911
  )
2825
2912
  : null;
2826
- const { chunks, results } = await applyBatchOpsInChunks(
2913
+ const { chunks, results, references } = await applyBatchOpsInChunks(
2827
2914
  ctx,
2828
2915
  rows.map((data, index) => ({
2829
2916
  type: "entries.create",
@@ -2840,6 +2927,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2840
2927
  idempotencyKey,
2841
2928
  allowUnknown,
2842
2929
  seedKeys ? onConflict : undefined,
2930
+ true,
2843
2931
  );
2844
2932
  // Only claim per-row links when we actually got a result per row. A
2845
2933
  // silently-empty `results` is indistinguishable from "this object has
@@ -2848,7 +2936,8 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2848
2936
  const linked = results.length;
2849
2937
  const statusCounts =
2850
2938
  seedKeys && results.length > 0 ? countResultStatuses(results) : null;
2851
- out(ctx, {
2939
+ outWithReferences(ctx, {
2940
+ ...boundWorkspaceReferences(references, results.length),
2852
2941
  created: statusCounts ? statusCounts.created : rows.length,
2853
2942
  ...(statusCounts
2854
2943
  ? {
@@ -3217,6 +3306,7 @@ async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
3217
3306
 
3218
3307
  async function runQueryCommand(ctx: CrmCliContext): Promise<void> {
3219
3308
  const objectName = shift(ctx.args, "object name");
3309
+ const withReferences = hasFlag(ctx.args, "--with-references");
3220
3310
  const filter = parseJson(getFlag(ctx.args, "--where")) as
3221
3311
  | JsonRecord
3222
3312
  | undefined;
@@ -3240,16 +3330,32 @@ async function runQueryCommand(ctx: CrmCliContext): Promise<void> {
3240
3330
  const dsl = { filter, select, sort, limit: limit.effective };
3241
3331
  const hasDollarKey = filter ? containsDollarKey(filter) : false;
3242
3332
  const args = hasDollarKey
3243
- ? { objectName, dslJson: JSON.stringify(dsl), ...assigneeScopeArgs }
3244
- : { objectName, dsl, ...assigneeScopeArgs };
3245
- out(
3333
+ ? {
3334
+ objectName,
3335
+ dslJson: JSON.stringify(dsl),
3336
+ ...assigneeScopeArgs,
3337
+ ...(pageMode ? { withReferences: true } : {}),
3338
+ }
3339
+ : {
3340
+ objectName,
3341
+ dsl,
3342
+ ...assigneeScopeArgs,
3343
+ ...(pageMode ? { withReferences: true } : {}),
3344
+ };
3345
+ outWithReferences(
3246
3346
  ctx,
3247
3347
  pageMode
3248
3348
  ? await callQuery(ctx, api.query.scanEntries, {
3249
3349
  ...args,
3250
3350
  paginationOpts: { cursor: cursor ?? null, numItems: limit.effective },
3251
3351
  })
3252
- : await callAction(ctx, api.query.queryEntries, args),
3352
+ : await callAction(
3353
+ ctx,
3354
+ withReferences
3355
+ ? api.query.queryEntriesWithReferences
3356
+ : api.query.queryEntries,
3357
+ args,
3358
+ ),
3253
3359
  );
3254
3360
  }
3255
3361
 
@@ -3285,6 +3391,7 @@ async function runAggregateCommand(ctx: CrmCliContext): Promise<void> {
3285
3391
 
3286
3392
  async function runSearchCommand(ctx: CrmCliContext): Promise<void> {
3287
3393
  const objectName = getFlag(ctx.args, "--object");
3394
+ const withReferences = hasFlag(ctx.args, "--with-references");
3288
3395
  const selectRaw = getFlag(ctx.args, "--select");
3289
3396
  const select = selectRaw?.split(",").map((name) => name.trim());
3290
3397
  if (select && (select.length > 30 || select.some((name) => !name))) {
@@ -3303,14 +3410,23 @@ async function runSearchCommand(ctx: CrmCliContext): Promise<void> {
3303
3410
  assertNoUnknownFlags(ctx.args, "crm search");
3304
3411
  const text = ctx.args.join(" ").trim();
3305
3412
  if (!text) throw new CrmCliError("Search text required");
3306
- out(
3413
+ outWithReferences(
3307
3414
  ctx,
3308
- await callQuery(ctx, projected ? api.query.searchPage : api.query.search, {
3309
- text,
3310
- objectName,
3311
- limit: limit.effective,
3312
- ...(projected ? { select, cursor } : {}),
3313
- }),
3415
+ await callQuery(
3416
+ ctx,
3417
+ projected
3418
+ ? api.query.searchPage
3419
+ : withReferences
3420
+ ? api.query.searchWithReferences
3421
+ : api.query.search,
3422
+ {
3423
+ text,
3424
+ objectName,
3425
+ limit: limit.effective,
3426
+ ...(projected ? { withReferences: true } : {}),
3427
+ ...(projected ? { select, cursor } : {}),
3428
+ },
3429
+ ),
3314
3430
  );
3315
3431
  }
3316
3432
 
@@ -4788,7 +4904,7 @@ export function crmHelpText(): string {
4788
4904
  return `Usage: dench crm <subcommand>
4789
4905
 
4790
4906
  Objects (CRM tables):
4791
- dench crm objects list [--json] [--compact]
4907
+ dench crm objects list [--json] [--compact] [--with-references]
4792
4908
  dench crm objects get <name>
4793
4909
  dench crm objects create <name> [--description ...] [--default-view kanban] [--icon ...]
4794
4910
  dench crm objects update <name> [--default-view ...] [--icon ...] [--display-name "Clients"] [--display-name-singular "Client"] [--visibility workspace|owner_only|owner_plus_members]
@@ -4932,7 +5048,7 @@ Fields (columns):
4932
5048
  Update the color (e.g. "#3b82f6") for an existing enum option.
4933
5049
 
4934
5050
  Entries (rows):
4935
- dench crm entries list <object> [--limit N] [--with-meta [--cursor '<continueCursor>']] [--json]
5051
+ dench crm entries list <object> [--with-references] [--limit N] [--with-meta [--cursor '<continueCursor>']] [--json]
4936
5052
  [--assigned-to me|unassigned|<userId|email> | --mine] [--assignee-field <Field>]
4937
5053
  --with-meta includes readable pagination; totalCount is the entire object's catalog count,
4938
5054
  not a readable or assigned total. Continue only when continueCursor is returned, preserving filters.
@@ -5071,8 +5187,8 @@ Cells (single-field updates):
5071
5187
  dench crm cells append <object> <entryId> <field> '<JSON value>'
5072
5188
 
5073
5189
  Query DSL:
5074
- dench crm query <object> --where '{"Status":"New"}' --select Name,Email --sort -Score --limit 100
5075
- dench crm query <object> --page --select Name,Email --limit 100 [--cursor '<continueCursor>'] --json
5190
+ dench crm query <object> [--with-references] --where '{"Status":"New"}' --select Name,Email --sort -Score --limit 100
5191
+ dench crm query <object> [--with-references] --page --select Name,Email --limit 100 [--cursor '<continueCursor>'] --json
5076
5192
  Cursor pages use index order (omit --sort). Continue until isDone, including empty filtered pages.
5077
5193
  [--assigned-to me|unassigned|<userId|email> | --mine] [--assignee-field <Field>]
5078
5194
  --mine AND-s "assigned to me" onto --where without you having to
@@ -5096,7 +5212,7 @@ Aggregates:
5096
5212
  dench crm aggregate <object> --field Status [--sum Amount]
5097
5213
 
5098
5214
  Full-text search:
5099
- dench crm search [--object <name>] [--select "Name,Email"] [--limit N] [--page] [--cursor <cursor>] <text>
5215
+ dench crm search [--object <name>] [--select "Name,Email"] [--limit N] [--page] [--cursor <cursor>] [--with-references] <text>
5100
5216
  --select returns a compact page with stable IDs, selected cells, isDone
5101
5217
  and continueCursor. Follow cursors until isDone for complete results.
5102
5218
  Without --select/--page/--cursor the legacy full-row array is unchanged.
package/dench.ts CHANGED
@@ -5298,6 +5298,10 @@ async function main() {
5298
5298
  jsonOutput: json,
5299
5299
  sessionToken: authedRuntime.sessionToken,
5300
5300
  host: authedRuntime.host,
5301
+ workspaceSlug:
5302
+ authedRuntime.organization?.slug ??
5303
+ process.env.DENCH_ORG_SLUG?.trim() ??
5304
+ undefined,
5301
5305
  });
5302
5306
  return;
5303
5307
  }