@dench.com/cli 2.7.1 → 2.7.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 +308 -16
- package/dench.ts +7 -0
- package/lib/api-schemas.ts +14 -0
- package/package.json +1 -1
package/crm.ts
CHANGED
|
@@ -90,8 +90,109 @@ type CrmCliContext = {
|
|
|
90
90
|
sessionToken?: string;
|
|
91
91
|
runId?: string;
|
|
92
92
|
enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
93
|
+
/** App origin used to build absolute entry deep links (e.g. https://www.dench.com). */
|
|
94
|
+
host?: string;
|
|
95
|
+
/** Workspace slug for absolute entry deep links (e.g. dench). */
|
|
96
|
+
workspaceSlug?: string;
|
|
93
97
|
};
|
|
94
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Canonical CRM entry deep links.
|
|
101
|
+
*
|
|
102
|
+
* Absolute (shareable): `https://…/<slug>?path=<object>&entry=<object>:<id>`
|
|
103
|
+
* Relative (in-app): `/?path=<object>&entry=<object>:<id>`
|
|
104
|
+
*
|
|
105
|
+
* Never invent `?object=` or a bare `entry=<id>` without the object prefix.
|
|
106
|
+
* `view` / `hidden` are optional UI chrome — not part of the canonical link.
|
|
107
|
+
*/
|
|
108
|
+
export function buildCrmEntryDeepLink(args: {
|
|
109
|
+
host?: string;
|
|
110
|
+
workspaceSlug?: string;
|
|
111
|
+
objectName: string;
|
|
112
|
+
entryId: string;
|
|
113
|
+
}): { href: string; url?: string } {
|
|
114
|
+
const objectName = args.objectName.trim();
|
|
115
|
+
const entryId = args.entryId.trim();
|
|
116
|
+
// Keep a literal `:` between object and id (docs + in-app links); only
|
|
117
|
+
// encode the segments themselves so Convex ids stay readable.
|
|
118
|
+
const entryParam = `${encodeURIComponent(objectName)}:${encodeURIComponent(entryId)}`;
|
|
119
|
+
const pathParam = encodeURIComponent(objectName);
|
|
120
|
+
const href = `/?path=${pathParam}&entry=${entryParam}`;
|
|
121
|
+
const slug = args.workspaceSlug?.trim();
|
|
122
|
+
const host = args.host?.trim();
|
|
123
|
+
if (!slug || !host) {
|
|
124
|
+
return { href };
|
|
125
|
+
}
|
|
126
|
+
const withProtocol = /^https?:\/\//.test(host) ? host : `https://${host}`;
|
|
127
|
+
let base: string;
|
|
128
|
+
try {
|
|
129
|
+
base = new URL(withProtocol).origin;
|
|
130
|
+
} catch {
|
|
131
|
+
return { href };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
href,
|
|
135
|
+
url: `${base}/${encodeURIComponent(slug)}?path=${pathParam}&entry=${entryParam}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function entryIdFromUnknown(value: unknown): string | undefined {
|
|
140
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const record = value as JsonRecord;
|
|
144
|
+
for (const key of ["entryId", "id"]) {
|
|
145
|
+
const candidate = record[key];
|
|
146
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
147
|
+
return candidate.trim();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Canonical object name echoed by `entries.get` / `entries.create` /
|
|
155
|
+
* `batch.apply`. The server resolves display labels, case, and plurals
|
|
156
|
+
* (`lib/crm/resolveObject.ts`), so the name the caller typed is often NOT
|
|
157
|
+
* the one the workspace URL resolver matches on — it compares object names
|
|
158
|
+
* exactly. Always prefer this over the raw CLI argument.
|
|
159
|
+
*/
|
|
160
|
+
function canonicalObjectNameFromUnknown(value: unknown): string | undefined {
|
|
161
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const candidate = (value as JsonRecord).objectName;
|
|
165
|
+
return typeof candidate === "string" && candidate.trim()
|
|
166
|
+
? candidate.trim()
|
|
167
|
+
: undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function withCrmEntryDeepLinks(
|
|
171
|
+
ctx: CrmCliContext,
|
|
172
|
+
fallbackObjectName: string,
|
|
173
|
+
value: unknown,
|
|
174
|
+
): unknown {
|
|
175
|
+
const entryId = entryIdFromUnknown(value);
|
|
176
|
+
if (!entryId || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
// Older deployments don't echo `objectName`; the raw arg is the only
|
|
180
|
+
// thing left, and it's correct whenever the caller used the canonical name.
|
|
181
|
+
const objectName =
|
|
182
|
+
canonicalObjectNameFromUnknown(value) ?? fallbackObjectName;
|
|
183
|
+
const links = buildCrmEntryDeepLink({
|
|
184
|
+
host: ctx.host,
|
|
185
|
+
workspaceSlug: ctx.workspaceSlug,
|
|
186
|
+
objectName,
|
|
187
|
+
entryId,
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
...(value as JsonRecord),
|
|
191
|
+
href: links.href,
|
|
192
|
+
...(links.url ? { url: links.url } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
95
196
|
// Adapt the shared helpers to throw CrmCliError so existing call sites
|
|
96
197
|
// don't need to change their try/catch shape.
|
|
97
198
|
class CrmCliError extends Error {}
|
|
@@ -291,18 +392,22 @@ async function applyBatchOpsInChunks(
|
|
|
291
392
|
ops: CrmBatchOp[],
|
|
292
393
|
idempotencyKey: string | undefined,
|
|
293
394
|
allowUnknown = false,
|
|
294
|
-
): Promise<number> {
|
|
395
|
+
): Promise<{ chunks: number; results: unknown[] }> {
|
|
295
396
|
const chunks = chunkArray(ops, CRM_BATCH_CHUNK_SIZE);
|
|
397
|
+
const results: unknown[] = [];
|
|
296
398
|
for (const [chunkIndex, chunk] of chunks.entries()) {
|
|
297
399
|
const chunkKey = chunkIdempotencyKey(idempotencyKey, chunkIndex);
|
|
298
|
-
await callMutation(ctx, api.batch.apply, {
|
|
400
|
+
const batchResult = (await callMutation(ctx, api.batch.apply, {
|
|
299
401
|
ops: chunk,
|
|
300
402
|
...runContextArgs(ctx),
|
|
301
403
|
...(chunkKey ? { idempotencyKey: chunkKey } : {}),
|
|
302
404
|
...(allowUnknown ? { allowUnknown: true } : {}),
|
|
303
|
-
});
|
|
405
|
+
})) as { results?: unknown[] } | null | undefined;
|
|
406
|
+
if (Array.isArray(batchResult?.results)) {
|
|
407
|
+
results.push(...batchResult.results);
|
|
408
|
+
}
|
|
304
409
|
}
|
|
305
|
-
return chunks.length;
|
|
410
|
+
return { chunks: chunks.length, results };
|
|
306
411
|
}
|
|
307
412
|
|
|
308
413
|
function runContextArgs(ctx: CrmCliContext): Record<string, string> {
|
|
@@ -568,6 +673,20 @@ const api = {
|
|
|
568
673
|
deleteView: makeFunctionReference<"mutation">(
|
|
569
674
|
"functions/crm/objects:deleteView",
|
|
570
675
|
),
|
|
676
|
+
setArchived: makeFunctionReference<"mutation">(
|
|
677
|
+
"functions/crm/objects:setArchived",
|
|
678
|
+
),
|
|
679
|
+
setViewPinned: makeFunctionReference<"mutation">(
|
|
680
|
+
"functions/crm/objects:setViewPinned",
|
|
681
|
+
),
|
|
682
|
+
},
|
|
683
|
+
sidebar: {
|
|
684
|
+
describe: makeFunctionReference<"query">(
|
|
685
|
+
"functions/workspaceNav:describeMySidebar",
|
|
686
|
+
),
|
|
687
|
+
reposition: makeFunctionReference<"mutation">(
|
|
688
|
+
"functions/workspaceNav:repositionMyNavItem",
|
|
689
|
+
),
|
|
571
690
|
},
|
|
572
691
|
fields: {
|
|
573
692
|
list: makeFunctionReference<"query">("functions/crm/fields:list"),
|
|
@@ -672,6 +791,8 @@ export async function runCrmCommand(opts: {
|
|
|
672
791
|
args: string[];
|
|
673
792
|
sessionToken?: string;
|
|
674
793
|
enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
794
|
+
host?: string;
|
|
795
|
+
workspaceSlug?: string;
|
|
675
796
|
}): Promise<void> {
|
|
676
797
|
const args = [...opts.args];
|
|
677
798
|
const jsonOutput = hasFlag(args, "--json");
|
|
@@ -682,6 +803,8 @@ export async function runCrmCommand(opts: {
|
|
|
682
803
|
sessionToken: opts.sessionToken,
|
|
683
804
|
runId: process.env.DENCH_RUN_ID?.trim() || undefined,
|
|
684
805
|
enrichmentGateway: opts.enrichmentGateway,
|
|
806
|
+
host: opts.host,
|
|
807
|
+
workspaceSlug: opts.workspaceSlug,
|
|
685
808
|
};
|
|
686
809
|
const subcommand = args.shift();
|
|
687
810
|
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
@@ -693,6 +816,8 @@ export async function runCrmCommand(opts: {
|
|
|
693
816
|
return await runObjectsCommand(ctx);
|
|
694
817
|
case "views":
|
|
695
818
|
return await runViewsCommand(ctx);
|
|
819
|
+
case "sidebar":
|
|
820
|
+
return await runSidebarCommand(ctx);
|
|
696
821
|
case "fields":
|
|
697
822
|
return await runFieldsCommand(ctx);
|
|
698
823
|
case "entries":
|
|
@@ -833,6 +958,50 @@ async function runCrmMembersCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
833
958
|
});
|
|
834
959
|
}
|
|
835
960
|
|
|
961
|
+
/**
|
|
962
|
+
* `dench crm sidebar …` — the workspace app rail.
|
|
963
|
+
*
|
|
964
|
+
* Row VISIBILITY is team-shared and lives on the CRM records themselves
|
|
965
|
+
* (`crm objects archive`, `crm views pin/archive`). Row ORDER is a
|
|
966
|
+
* per-member preference, which is what `move` writes. `list` shows both
|
|
967
|
+
* halves: the visible rows with their 1-based positions, plus what's
|
|
968
|
+
* hidden and why.
|
|
969
|
+
*/
|
|
970
|
+
async function runSidebarCommand(ctx: CrmCliContext): Promise<void> {
|
|
971
|
+
const verb = ctx.args.shift() ?? "list";
|
|
972
|
+
switch (verb) {
|
|
973
|
+
case "list": {
|
|
974
|
+
assertNoUnknownFlags(ctx.args, "crm sidebar list");
|
|
975
|
+
out(ctx, await callQuery(ctx, api.sidebar.describe, {}));
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
case "move": {
|
|
979
|
+
const navId = shift(ctx.args, "nav id");
|
|
980
|
+
const rawPosition = shift(ctx.args, "position");
|
|
981
|
+
assertNoUnknownFlags(ctx.args, "crm sidebar move");
|
|
982
|
+
let position: string | number;
|
|
983
|
+
if (rawPosition === "top" || rawPosition === "bottom") {
|
|
984
|
+
position = rawPosition;
|
|
985
|
+
} else {
|
|
986
|
+
const parsed = Number.parseInt(rawPosition, 10);
|
|
987
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
988
|
+
throw new CrmCliError(
|
|
989
|
+
`position must be "top", "bottom", or a positive integer, got ${rawPosition}`,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
position = parsed;
|
|
993
|
+
}
|
|
994
|
+
out(
|
|
995
|
+
ctx,
|
|
996
|
+
await callMutation(ctx, api.sidebar.reposition, { navId, position }),
|
|
997
|
+
);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
default:
|
|
1001
|
+
throw new CrmCliError(`Unknown crm sidebar verb: ${verb}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
836
1005
|
async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
|
|
837
1006
|
const verb = ctx.args.shift();
|
|
838
1007
|
switch (verb) {
|
|
@@ -902,6 +1071,22 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
902
1071
|
out(ctx, await callMutation(ctx, api.objects.remove, { name }));
|
|
903
1072
|
return;
|
|
904
1073
|
}
|
|
1074
|
+
// Archive is the sidebar's "remove from my rail", NOT a delete: the
|
|
1075
|
+
// table, its rows and every link to it keep working, the row just
|
|
1076
|
+
// moves into the rail's Archived flyout. Team-shared.
|
|
1077
|
+
case "archive":
|
|
1078
|
+
case "unarchive": {
|
|
1079
|
+
const name = shift(ctx.args, "object name");
|
|
1080
|
+
assertNoUnknownFlags(ctx.args, `crm objects ${verb}`);
|
|
1081
|
+
out(
|
|
1082
|
+
ctx,
|
|
1083
|
+
await callMutation(ctx, api.objects.setArchived, {
|
|
1084
|
+
name,
|
|
1085
|
+
archived: verb === "archive",
|
|
1086
|
+
}),
|
|
1087
|
+
);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
905
1090
|
default:
|
|
906
1091
|
throw new CrmCliError(`Unknown crm objects verb: ${verb ?? "<none>"}`);
|
|
907
1092
|
}
|
|
@@ -1363,6 +1548,39 @@ async function runViewsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1363
1548
|
);
|
|
1364
1549
|
return;
|
|
1365
1550
|
}
|
|
1551
|
+
// Only `pinned` views render as workspace sidebar rows. Pinning also
|
|
1552
|
+
// clears any archive stamp, since a pinned-but-archived view still
|
|
1553
|
+
// wouldn't show up.
|
|
1554
|
+
case "pin":
|
|
1555
|
+
case "unpin": {
|
|
1556
|
+
const objectName = shift(ctx.args, "object name");
|
|
1557
|
+
const viewName = shift(ctx.args, "view name");
|
|
1558
|
+
assertNoUnknownFlags(ctx.args, `crm views ${verb}`);
|
|
1559
|
+
out(
|
|
1560
|
+
ctx,
|
|
1561
|
+
await callMutation(ctx, api.objects.setViewPinned, {
|
|
1562
|
+
objectName,
|
|
1563
|
+
viewName,
|
|
1564
|
+
pinned: verb === "pin",
|
|
1565
|
+
}),
|
|
1566
|
+
);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
case "archive":
|
|
1570
|
+
case "unarchive": {
|
|
1571
|
+
const objectName = shift(ctx.args, "object name");
|
|
1572
|
+
const viewName = shift(ctx.args, "view name");
|
|
1573
|
+
assertNoUnknownFlags(ctx.args, `crm views ${verb}`);
|
|
1574
|
+
out(
|
|
1575
|
+
ctx,
|
|
1576
|
+
await callMutation(ctx, api.objects.setArchived, {
|
|
1577
|
+
name: objectName,
|
|
1578
|
+
viewName,
|
|
1579
|
+
archived: verb === "archive",
|
|
1580
|
+
}),
|
|
1581
|
+
);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1366
1584
|
default:
|
|
1367
1585
|
throw new CrmCliError(`Unknown crm views verb: ${verb ?? "<none>"}`);
|
|
1368
1586
|
}
|
|
@@ -1624,10 +1842,14 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1624
1842
|
assertNoUnknownFlags(ctx.args, "crm entries get");
|
|
1625
1843
|
out(
|
|
1626
1844
|
ctx,
|
|
1627
|
-
|
|
1845
|
+
withCrmEntryDeepLinks(
|
|
1846
|
+
ctx,
|
|
1628
1847
|
objectName,
|
|
1629
|
-
|
|
1630
|
-
|
|
1848
|
+
await callQuery(ctx, api.entries.get, {
|
|
1849
|
+
objectName,
|
|
1850
|
+
entryId: entryId as any,
|
|
1851
|
+
}),
|
|
1852
|
+
),
|
|
1631
1853
|
);
|
|
1632
1854
|
return;
|
|
1633
1855
|
}
|
|
@@ -1657,12 +1879,16 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1657
1879
|
// pre-fetch — that would add a round-trip to every create.
|
|
1658
1880
|
out(
|
|
1659
1881
|
ctx,
|
|
1660
|
-
|
|
1882
|
+
withCrmEntryDeepLinks(
|
|
1883
|
+
ctx,
|
|
1661
1884
|
objectName,
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1885
|
+
await callMutation(ctx, api.entries.create, {
|
|
1886
|
+
objectName,
|
|
1887
|
+
data,
|
|
1888
|
+
...runContextArgs(ctx),
|
|
1889
|
+
...(allowUnknown ? { allowUnknown: true } : {}),
|
|
1890
|
+
}),
|
|
1891
|
+
),
|
|
1666
1892
|
);
|
|
1667
1893
|
return;
|
|
1668
1894
|
}
|
|
@@ -1680,7 +1906,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1680
1906
|
}),
|
|
1681
1907
|
);
|
|
1682
1908
|
await assertKnownDataKeys(ctx, objectName, rows, allowUnknown);
|
|
1683
|
-
const chunks = await applyBatchOpsInChunks(
|
|
1909
|
+
const { chunks, results } = await applyBatchOpsInChunks(
|
|
1684
1910
|
ctx,
|
|
1685
1911
|
rows.map((data) => ({
|
|
1686
1912
|
type: "entries.create",
|
|
@@ -1690,7 +1916,41 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1690
1916
|
idempotencyKey,
|
|
1691
1917
|
allowUnknown,
|
|
1692
1918
|
);
|
|
1693
|
-
|
|
1919
|
+
// Only claim per-row links when we actually got a result per row. A
|
|
1920
|
+
// silently-empty `results` is indistinguishable from "this object has
|
|
1921
|
+
// no links" and sends the agent right back to inventing URLs — the
|
|
1922
|
+
// exact failure this command's `url`/`href` output exists to prevent.
|
|
1923
|
+
const linked = results.length;
|
|
1924
|
+
out(ctx, {
|
|
1925
|
+
created: rows.length,
|
|
1926
|
+
chunks,
|
|
1927
|
+
...(linked > 0
|
|
1928
|
+
? {
|
|
1929
|
+
results: results.map((result) =>
|
|
1930
|
+
withCrmEntryDeepLinks(ctx, objectName, result),
|
|
1931
|
+
),
|
|
1932
|
+
}
|
|
1933
|
+
: {}),
|
|
1934
|
+
...(linked === rows.length
|
|
1935
|
+
? {}
|
|
1936
|
+
: {
|
|
1937
|
+
linksUnavailable: true,
|
|
1938
|
+
// The recovery path has to be followable. With zero results
|
|
1939
|
+
// there is no entryId to hand to `entries get`, so point at
|
|
1940
|
+
// `entries list` (it returns `id` per row) instead — a hint
|
|
1941
|
+
// the agent can't act on just sends it back to guessing.
|
|
1942
|
+
linksHint:
|
|
1943
|
+
`Got ${linked} link(s) for ${rows.length} row(s). ` +
|
|
1944
|
+
(linked === 0
|
|
1945
|
+
? `Run \`dench crm entries list ${objectName} --json\` to get each row's ` +
|
|
1946
|
+
`\`id\`, then \`dench crm entries get ${objectName} <id> --json\` for its ` +
|
|
1947
|
+
"canonical url"
|
|
1948
|
+
: `The rows above carry \`url\`/\`href\`; for the rest run ` +
|
|
1949
|
+
`\`dench crm entries list ${objectName} --json\` to get their \`id\`, then ` +
|
|
1950
|
+
`\`dench crm entries get ${objectName} <id> --json\``) +
|
|
1951
|
+
" — do not construct a url by hand.",
|
|
1952
|
+
}),
|
|
1953
|
+
});
|
|
1694
1954
|
return;
|
|
1695
1955
|
}
|
|
1696
1956
|
case "update": {
|
|
@@ -1743,7 +2003,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1743
2003
|
updates.map((update) => update.data),
|
|
1744
2004
|
allowUnknown,
|
|
1745
2005
|
);
|
|
1746
|
-
const chunks = await applyBatchOpsInChunks(
|
|
2006
|
+
const { chunks } = await applyBatchOpsInChunks(
|
|
1747
2007
|
ctx,
|
|
1748
2008
|
updates.map((update) => ({
|
|
1749
2009
|
type: "entries.update",
|
|
@@ -1874,7 +2134,7 @@ async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1874
2134
|
data,
|
|
1875
2135
|
}),
|
|
1876
2136
|
);
|
|
1877
|
-
const chunks = await applyBatchOpsInChunks(ctx, ops, idempotencyKey);
|
|
2137
|
+
const { chunks } = await applyBatchOpsInChunks(ctx, ops, idempotencyKey);
|
|
1878
2138
|
out(ctx, { updated: updates.length, entriesUpdated: ops.length, chunks });
|
|
1879
2139
|
return;
|
|
1880
2140
|
}
|
|
@@ -3287,6 +3547,10 @@ Objects (CRM tables):
|
|
|
3287
3547
|
command/API identifier (works on protected objects too).
|
|
3288
3548
|
dench crm objects rename <from> <to>
|
|
3289
3549
|
dench crm objects delete <name>
|
|
3550
|
+
dench crm objects archive <name> | unarchive <name>
|
|
3551
|
+
Hides/restores the object's workspace SIDEBAR row. Not a delete —
|
|
3552
|
+
the table, its rows and every link keep working; the row just moves
|
|
3553
|
+
into the rail's Archived flyout. Team-shared.
|
|
3290
3554
|
|
|
3291
3555
|
Views (saved CRM subsets):
|
|
3292
3556
|
dench crm views list <object> [--json]
|
|
@@ -3306,6 +3570,22 @@ Views (saved CRM subsets):
|
|
|
3306
3570
|
dench crm views delete <object> <view-name>
|
|
3307
3571
|
Idempotent; also clears the active-view pointer if it referenced
|
|
3308
3572
|
the deleted view.
|
|
3573
|
+
dench crm views pin <object> <view-name> | unpin <object> <view-name>
|
|
3574
|
+
Only pinned views render as workspace sidebar rows. Pinning also
|
|
3575
|
+
clears any archive stamp.
|
|
3576
|
+
dench crm views archive <object> <view-name> | unarchive <object> <view-name>
|
|
3577
|
+
Moves the view's sidebar row into the Archived flyout. Unarchiving
|
|
3578
|
+
re-asserts the pin so the row comes back.
|
|
3579
|
+
|
|
3580
|
+
Sidebar (the workspace app rail):
|
|
3581
|
+
dench crm sidebar list [--json]
|
|
3582
|
+
Visible rows in order with 1-based positions, plus what's hidden
|
|
3583
|
+
from the rail and why (archived vs. an unpinned view).
|
|
3584
|
+
dench crm sidebar move <nav-id> <top|bottom|N>
|
|
3585
|
+
Reposition one row. Nav ids come from 'sidebar list' — e.g.
|
|
3586
|
+
'crm-object:task', 'crm-view:task:Due%20today', 'crm-people',
|
|
3587
|
+
'automation'. Row ORDER is per-member; row visibility (above) is
|
|
3588
|
+
team-shared.
|
|
3309
3589
|
|
|
3310
3590
|
Fields (columns):
|
|
3311
3591
|
dench crm fields list <object> [--json]
|
|
@@ -3368,6 +3648,18 @@ Entries (rows):
|
|
|
3368
3648
|
being silently dropped. Verify names with 'crm fields list <object>'
|
|
3369
3649
|
first. Pass --allow-unknown to write the raw value anyway, and on
|
|
3370
3650
|
create the object's required fields must be present.
|
|
3651
|
+
create / get / create-many also return deep links when the
|
|
3652
|
+
workspace slug is known: \`url\` (absolute shareable) and \`href\`
|
|
3653
|
+
(relative in-app). Paste those — never invent \`?object=\` or a
|
|
3654
|
+
bare \`entry=<id>\` without the \`<object>:\` prefix. Canonical form:
|
|
3655
|
+
/<slug>?path=<object>&entry=<object>:<entryId>
|
|
3656
|
+
The link uses the object's CANONICAL name (echoed back as
|
|
3657
|
+
\`objectName\`), not the alias you typed — \`create Tasks\` resolves
|
|
3658
|
+
server-side but only \`task\` opens the record. If a response has no
|
|
3659
|
+
\`url\`/\`href\`, run 'crm entries get <object> <entryId> --json' for
|
|
3660
|
+
one. If create-many reports \`linksUnavailable\`, the unlinked rows
|
|
3661
|
+
have no id in that response — run 'crm entries list <object> --json'
|
|
3662
|
+
to recover their \`id\` first.
|
|
3371
3663
|
|
|
3372
3664
|
Cells (single-field updates):
|
|
3373
3665
|
dench crm cells get <object> <entryId> <field>
|
package/dench.ts
CHANGED
|
@@ -4035,6 +4035,8 @@ async function main() {
|
|
|
4035
4035
|
convex: runtime.client,
|
|
4036
4036
|
args: subArgs,
|
|
4037
4037
|
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
4038
|
+
host: resolveApiHost(LOCAL_HOST),
|
|
4039
|
+
workspaceSlug: runtime.workspaceArgs.workspaceSlug,
|
|
4038
4040
|
enrichmentGateway: gatewayAuth
|
|
4039
4041
|
? {
|
|
4040
4042
|
apiKey: gatewayAuth.bearerToken,
|
|
@@ -4053,6 +4055,11 @@ async function main() {
|
|
|
4053
4055
|
convex: authedRuntime.client,
|
|
4054
4056
|
args: subArgs,
|
|
4055
4057
|
sessionToken: authedRuntime.sessionToken,
|
|
4058
|
+
host: authedRuntime.host,
|
|
4059
|
+
workspaceSlug:
|
|
4060
|
+
authedRuntime.organization?.slug ??
|
|
4061
|
+
process.env.DENCH_ORG_SLUG?.trim() ??
|
|
4062
|
+
undefined,
|
|
4056
4063
|
enrichmentGateway: gatewayAuth
|
|
4057
4064
|
? {
|
|
4058
4065
|
apiKey: gatewayAuth.bearerToken,
|
package/lib/api-schemas.ts
CHANGED
|
@@ -2126,8 +2126,14 @@ export const responseSchemas: Record<string, ZodTypeAny> = {
|
|
|
2126
2126
|
id: z.string(),
|
|
2127
2127
|
role: z.enum(["user", "assistant", "system"]),
|
|
2128
2128
|
createdAt: z.number(),
|
|
2129
|
+
updatedAt: z.number(),
|
|
2129
2130
|
text: z.string(),
|
|
2130
2131
|
runId: z.string().nullable(),
|
|
2132
|
+
status: z
|
|
2133
|
+
.enum(["streaming", "complete", "error", "canceled"])
|
|
2134
|
+
.nullable(),
|
|
2135
|
+
revision: z.number(),
|
|
2136
|
+
isDraft: z.boolean(),
|
|
2131
2137
|
}),
|
|
2132
2138
|
),
|
|
2133
2139
|
}),
|
|
@@ -3130,15 +3136,23 @@ export const responseExamples: Record<string, unknown> = {
|
|
|
3130
3136
|
id: "cm_def456",
|
|
3131
3137
|
role: "user",
|
|
3132
3138
|
createdAt: 1765526400000,
|
|
3139
|
+
updatedAt: 1765526400000,
|
|
3133
3140
|
text: "How do we deploy?",
|
|
3134
3141
|
runId: null,
|
|
3142
|
+
status: "complete",
|
|
3143
|
+
revision: 0,
|
|
3144
|
+
isDraft: false,
|
|
3135
3145
|
},
|
|
3136
3146
|
{
|
|
3137
3147
|
id: "cm_ghi789",
|
|
3138
3148
|
role: "assistant",
|
|
3139
3149
|
createdAt: 1765526460000,
|
|
3150
|
+
updatedAt: 1765526460000,
|
|
3140
3151
|
text: "Push to main; Vercel auto-deploys.",
|
|
3141
3152
|
runId: "rn_jkl012",
|
|
3153
|
+
status: "complete",
|
|
3154
|
+
revision: 1,
|
|
3155
|
+
isDraft: false,
|
|
3142
3156
|
},
|
|
3143
3157
|
],
|
|
3144
3158
|
},
|