@dench.com/cli 3.8.6-staging.409 → 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/app-sdk.d.ts +788 -26
- package/campaign.ts +94 -27
- package/crm.ts +157 -41
- package/dench.ts +4 -0
- package/lib/api-schemas.ts +126 -16
- package/lib/app-sdk-catalog.generated.ts +5 -5
- package/lib/campaign-agent.ts +26 -14
- package/lib/campaign-reference.ts +39 -0
- package/lib/command-registry.ts +10 -6
- package/lib/references.generated.json +14 -14
- package/lib/workspace-references.ts +205 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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,
|
|
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(
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
1555
|
+
const data = await callQuery(
|
|
1492
1556
|
ctx,
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2571
|
+
outWithReferences(
|
|
2500
2572
|
ctx,
|
|
2501
|
-
await callQuery(
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
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
|
-
|
|
2548
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
? {
|
|
3244
|
-
|
|
3245
|
-
|
|
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(
|
|
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
|
-
|
|
3413
|
+
outWithReferences(
|
|
3307
3414
|
ctx,
|
|
3308
|
-
await callQuery(
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
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
|
}
|