@dench.com/cli 2.8.7-staging.189 → 2.8.8-staging.192
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-comments.ts +303 -0
- package/crm.ts +46 -7
- package/dench.ts +44 -0
- package/lib/api-schemas.ts +325 -1
- package/lib/command-registry.ts +303 -0
- package/lib/enrichment-gateway.ts +21 -1
- package/notifications.ts +262 -0
- package/package.json +3 -1
package/crm-comments.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dench crm comments …`, `dench crm watch …`, `dench crm tasks …`
|
|
3
|
+
*
|
|
4
|
+
* Thin dispatchers over the Convex functions the entry panel itself uses
|
|
5
|
+
* (`functions/crm/comments`, `functions/crm/tasks`), so the CLI, the
|
|
6
|
+
* `/api/v1` surface, and the UI share one contract. Kept out of `crm.ts`
|
|
7
|
+
* only for file size.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { makeFunctionReference } from "convex/server";
|
|
12
|
+
import {
|
|
13
|
+
assertNoUnknownFlags,
|
|
14
|
+
type CrmCliContext,
|
|
15
|
+
CrmCliError,
|
|
16
|
+
callMutation,
|
|
17
|
+
callQuery,
|
|
18
|
+
out,
|
|
19
|
+
shift,
|
|
20
|
+
} from "./crm";
|
|
21
|
+
import { getFlag, hasFlag } from "./lib/cli-args";
|
|
22
|
+
|
|
23
|
+
const api = {
|
|
24
|
+
comments: {
|
|
25
|
+
listForEntry: makeFunctionReference<"query">(
|
|
26
|
+
"functions/crm/comments:listForEntry",
|
|
27
|
+
),
|
|
28
|
+
listReplies: makeFunctionReference<"query">(
|
|
29
|
+
"functions/crm/comments:listReplies",
|
|
30
|
+
),
|
|
31
|
+
get: makeFunctionReference<"query">("functions/crm/comments:get"),
|
|
32
|
+
create: makeFunctionReference<"mutation">("functions/crm/comments:create"),
|
|
33
|
+
edit: makeFunctionReference<"mutation">("functions/crm/comments:edit"),
|
|
34
|
+
remove: makeFunctionReference<"mutation">("functions/crm/comments:remove"),
|
|
35
|
+
toggleReaction: makeFunctionReference<"mutation">(
|
|
36
|
+
"functions/crm/comments:toggleReaction",
|
|
37
|
+
),
|
|
38
|
+
getWatchState: makeFunctionReference<"query">(
|
|
39
|
+
"functions/crm/comments:getWatchState",
|
|
40
|
+
),
|
|
41
|
+
setWatching: makeFunctionReference<"mutation">(
|
|
42
|
+
"functions/crm/comments:setWatching",
|
|
43
|
+
),
|
|
44
|
+
},
|
|
45
|
+
tasks: {
|
|
46
|
+
listMyOpenTasks: makeFunctionReference<"query">(
|
|
47
|
+
"functions/crm/tasks:listMyOpenTasks",
|
|
48
|
+
),
|
|
49
|
+
listEntryTasks: makeFunctionReference<"query">(
|
|
50
|
+
"functions/crm/tasks:listEntryTasks",
|
|
51
|
+
),
|
|
52
|
+
createEntryTask: makeFunctionReference<"mutation">(
|
|
53
|
+
"functions/crm/tasks:createEntryTask",
|
|
54
|
+
),
|
|
55
|
+
completeTask: makeFunctionReference<"mutation">(
|
|
56
|
+
"functions/crm/tasks:completeTask",
|
|
57
|
+
),
|
|
58
|
+
reopenTask: makeFunctionReference<"mutation">(
|
|
59
|
+
"functions/crm/tasks:reopenTask",
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const DENCH_MENTION = "[@Dench](/?member=dench)";
|
|
65
|
+
|
|
66
|
+
function paginationFlags(
|
|
67
|
+
args: string[],
|
|
68
|
+
defaultLimit: number,
|
|
69
|
+
): { numItems: number; cursor: string | null } {
|
|
70
|
+
const rawLimit = getFlag(args, "--limit");
|
|
71
|
+
const limit =
|
|
72
|
+
rawLimit === undefined ? defaultLimit : Number.parseInt(rawLimit, 10);
|
|
73
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
74
|
+
throw new CrmCliError("--limit must be a positive integer");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
numItems: Math.min(limit, 100),
|
|
78
|
+
cursor: getFlag(args, "--cursor") ?? null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `--body`, `--body-file`, or piped stdin — in that order. */
|
|
83
|
+
async function readBody(args: string[]): Promise<string> {
|
|
84
|
+
const inline = getFlag(args, "--body");
|
|
85
|
+
if (inline !== undefined) return inline;
|
|
86
|
+
const file = getFlag(args, "--body-file");
|
|
87
|
+
if (file) return await readFile(file, "utf8");
|
|
88
|
+
if (!process.stdin.isTTY) {
|
|
89
|
+
const chunks: Buffer[] = [];
|
|
90
|
+
for await (const chunk of process.stdin) {
|
|
91
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
92
|
+
}
|
|
93
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
94
|
+
if (text.trim()) return text;
|
|
95
|
+
}
|
|
96
|
+
throw new CrmCliError(
|
|
97
|
+
'A comment body is required: pass --body "…", --body-file <path>, or pipe markdown on stdin.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Repeated `--mention <userId>` flags. */
|
|
102
|
+
function collectMentions(args: string[]): string[] {
|
|
103
|
+
const ids: string[] = [];
|
|
104
|
+
for (;;) {
|
|
105
|
+
const value = getFlag(args, "--mention");
|
|
106
|
+
if (value === undefined) break;
|
|
107
|
+
if (value.trim()) ids.push(value.trim());
|
|
108
|
+
}
|
|
109
|
+
return ids;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function runCommentsCommand(ctx: CrmCliContext): Promise<void> {
|
|
113
|
+
const verb = ctx.args.shift();
|
|
114
|
+
switch (verb) {
|
|
115
|
+
case "list": {
|
|
116
|
+
const objectName = shift(ctx.args, "object name");
|
|
117
|
+
const entryId = shift(ctx.args, "entry id");
|
|
118
|
+
const paginationOpts = paginationFlags(ctx.args, 20);
|
|
119
|
+
const rawReplies = getFlag(ctx.args, "--replies");
|
|
120
|
+
const previewReplies =
|
|
121
|
+
rawReplies === undefined ? undefined : Number.parseInt(rawReplies, 10);
|
|
122
|
+
assertNoUnknownFlags(ctx.args, "crm comments list");
|
|
123
|
+
out(
|
|
124
|
+
ctx,
|
|
125
|
+
await callQuery(ctx, api.comments.listForEntry, {
|
|
126
|
+
objectName,
|
|
127
|
+
entryId,
|
|
128
|
+
paginationOpts,
|
|
129
|
+
...(previewReplies !== undefined && Number.isFinite(previewReplies)
|
|
130
|
+
? { previewReplies }
|
|
131
|
+
: {}),
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case "replies": {
|
|
137
|
+
const rootCommentId = shift(ctx.args, "comment id");
|
|
138
|
+
const paginationOpts = paginationFlags(ctx.args, 50);
|
|
139
|
+
assertNoUnknownFlags(ctx.args, "crm comments replies");
|
|
140
|
+
out(
|
|
141
|
+
ctx,
|
|
142
|
+
await callQuery(ctx, api.comments.listReplies, {
|
|
143
|
+
rootCommentId,
|
|
144
|
+
paginationOpts,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
case "get": {
|
|
150
|
+
const commentId = shift(ctx.args, "comment id");
|
|
151
|
+
assertNoUnknownFlags(ctx.args, "crm comments get");
|
|
152
|
+
out(ctx, await callQuery(ctx, api.comments.get, { commentId }));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
case "create": {
|
|
156
|
+
const objectName = shift(ctx.args, "object name");
|
|
157
|
+
const entryId = shift(ctx.args, "entry id");
|
|
158
|
+
let body = await readBody(ctx.args);
|
|
159
|
+
const parentCommentId = getFlag(ctx.args, "--reply-to");
|
|
160
|
+
const mentionUserIds = collectMentions(ctx.args);
|
|
161
|
+
const mentionDench = hasFlag(ctx.args, "--dench");
|
|
162
|
+
assertNoUnknownFlags(ctx.args, "crm comments create");
|
|
163
|
+
if (mentionDench && !body.includes("/?member=dench")) {
|
|
164
|
+
body = `${DENCH_MENTION} ${body}`;
|
|
165
|
+
}
|
|
166
|
+
out(
|
|
167
|
+
ctx,
|
|
168
|
+
await callMutation(ctx, api.comments.create, {
|
|
169
|
+
objectName,
|
|
170
|
+
entryId,
|
|
171
|
+
body,
|
|
172
|
+
...(parentCommentId ? { parentCommentId } : {}),
|
|
173
|
+
...(mentionUserIds.length > 0 ? { mentionUserIds } : {}),
|
|
174
|
+
...(mentionDench ? { mentionDench: true } : {}),
|
|
175
|
+
...(ctx.runId ? { runId: ctx.runId } : {}),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
case "update": {
|
|
181
|
+
const commentId = shift(ctx.args, "comment id");
|
|
182
|
+
const body = await readBody(ctx.args);
|
|
183
|
+
assertNoUnknownFlags(ctx.args, "crm comments update");
|
|
184
|
+
out(ctx, await callMutation(ctx, api.comments.edit, { commentId, body }));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
case "delete": {
|
|
188
|
+
const commentId = shift(ctx.args, "comment id");
|
|
189
|
+
assertNoUnknownFlags(ctx.args, "crm comments delete");
|
|
190
|
+
out(ctx, await callMutation(ctx, api.comments.remove, { commentId }));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
case "react": {
|
|
194
|
+
const commentId = shift(ctx.args, "comment id");
|
|
195
|
+
const emoji = shift(ctx.args, "emoji");
|
|
196
|
+
assertNoUnknownFlags(ctx.args, "crm comments react");
|
|
197
|
+
out(
|
|
198
|
+
ctx,
|
|
199
|
+
await callMutation(ctx, api.comments.toggleReaction, {
|
|
200
|
+
commentId,
|
|
201
|
+
emoji,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
throw new CrmCliError(
|
|
208
|
+
`Unknown comments verb: ${verb ?? "(none)"}. Use list | replies | get | create | update | delete | react.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function runWatchCommand(ctx: CrmCliContext): Promise<void> {
|
|
214
|
+
if (ctx.args[0] === "status") {
|
|
215
|
+
ctx.args.shift();
|
|
216
|
+
const objectName = shift(ctx.args, "object name");
|
|
217
|
+
const entryId = shift(ctx.args, "entry id");
|
|
218
|
+
assertNoUnknownFlags(ctx.args, "crm watch status");
|
|
219
|
+
void objectName;
|
|
220
|
+
out(ctx, await callQuery(ctx, api.comments.getWatchState, { entryId }));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const objectName = shift(ctx.args, "object name");
|
|
224
|
+
const entryId = shift(ctx.args, "entry id");
|
|
225
|
+
const off = hasFlag(ctx.args, "--off");
|
|
226
|
+
assertNoUnknownFlags(ctx.args, "crm watch");
|
|
227
|
+
void objectName;
|
|
228
|
+
out(
|
|
229
|
+
ctx,
|
|
230
|
+
await callMutation(ctx, api.comments.setWatching, {
|
|
231
|
+
entryId,
|
|
232
|
+
watching: !off,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function runTasksCommand(ctx: CrmCliContext): Promise<void> {
|
|
238
|
+
const verb = ctx.args.shift();
|
|
239
|
+
switch (verb) {
|
|
240
|
+
case "mine": {
|
|
241
|
+
const rawLimit = getFlag(ctx.args, "--limit");
|
|
242
|
+
assertNoUnknownFlags(ctx.args, "crm tasks mine");
|
|
243
|
+
out(
|
|
244
|
+
ctx,
|
|
245
|
+
await callQuery(ctx, api.tasks.listMyOpenTasks, {
|
|
246
|
+
...(rawLimit ? { limit: Number.parseInt(rawLimit, 10) } : {}),
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
case "list": {
|
|
252
|
+
const objectName = shift(ctx.args, "object name");
|
|
253
|
+
const entryId = shift(ctx.args, "entry id");
|
|
254
|
+
assertNoUnknownFlags(ctx.args, "crm tasks list");
|
|
255
|
+
out(
|
|
256
|
+
ctx,
|
|
257
|
+
await callQuery(ctx, api.tasks.listEntryTasks, { objectName, entryId }),
|
|
258
|
+
);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
case "create": {
|
|
262
|
+
const objectName = shift(ctx.args, "object name");
|
|
263
|
+
const entryId = shift(ctx.args, "entry id");
|
|
264
|
+
const name = getFlag(ctx.args, "--name");
|
|
265
|
+
const assigneeUserId = getFlag(ctx.args, "--assignee");
|
|
266
|
+
const dueDateKey = getFlag(ctx.args, "--due");
|
|
267
|
+
assertNoUnknownFlags(ctx.args, "crm tasks create");
|
|
268
|
+
if (!name?.trim()) throw new CrmCliError("--name is required");
|
|
269
|
+
if (dueDateKey && !/^\d{4}-\d{2}-\d{2}$/.test(dueDateKey)) {
|
|
270
|
+
throw new CrmCliError("--due must be YYYY-MM-DD");
|
|
271
|
+
}
|
|
272
|
+
out(
|
|
273
|
+
ctx,
|
|
274
|
+
await callMutation(ctx, api.tasks.createEntryTask, {
|
|
275
|
+
objectName,
|
|
276
|
+
entryId,
|
|
277
|
+
name: name.trim(),
|
|
278
|
+
...(assigneeUserId ? { assigneeUserId } : {}),
|
|
279
|
+
...(dueDateKey ? { dueDateKey } : {}),
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
case "complete":
|
|
285
|
+
case "reopen": {
|
|
286
|
+
const entryId = shift(ctx.args, "task entry id");
|
|
287
|
+
assertNoUnknownFlags(ctx.args, `crm tasks ${verb}`);
|
|
288
|
+
out(
|
|
289
|
+
ctx,
|
|
290
|
+
await callMutation(
|
|
291
|
+
ctx,
|
|
292
|
+
verb === "complete" ? api.tasks.completeTask : api.tasks.reopenTask,
|
|
293
|
+
{ entryId },
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
throw new CrmCliError(
|
|
300
|
+
`Unknown tasks verb: ${verb ?? "(none)"}. Use mine | list | create | complete | reopen.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
package/crm.ts
CHANGED
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import type { ConvexHttpClient } from "convex/browser";
|
|
31
31
|
import { makeFunctionReference } from "convex/server";
|
|
32
|
+
import {
|
|
33
|
+
runCommentsCommand,
|
|
34
|
+
runTasksCommand,
|
|
35
|
+
runWatchCommand,
|
|
36
|
+
} from "./crm-comments";
|
|
32
37
|
import {
|
|
33
38
|
assertNoUnknownFlags as assertNoUnknownFlagsRaw,
|
|
34
39
|
CliArgError,
|
|
@@ -65,7 +70,7 @@ type JsonRecord = Record<string, unknown>;
|
|
|
65
70
|
|
|
66
71
|
const CRM_BATCH_CHUNK_SIZE = 200;
|
|
67
72
|
|
|
68
|
-
type CrmCliContext = {
|
|
73
|
+
export type CrmCliContext = {
|
|
69
74
|
convex: ConvexHttpClient;
|
|
70
75
|
args: string[];
|
|
71
76
|
jsonOutput: boolean;
|
|
@@ -196,7 +201,7 @@ function withCrmEntryDeepLinks(
|
|
|
196
201
|
|
|
197
202
|
// Adapt the shared helpers to throw CrmCliError so existing call sites
|
|
198
203
|
// don't need to change their try/catch shape.
|
|
199
|
-
class CrmCliError extends Error {}
|
|
204
|
+
export class CrmCliError extends Error {}
|
|
200
205
|
|
|
201
206
|
function userFacingConvexErrorMessage(error: unknown): string | null {
|
|
202
207
|
if (!error || typeof error !== "object") return null;
|
|
@@ -224,7 +229,7 @@ function normalizeRpcError(error: unknown): Error {
|
|
|
224
229
|
);
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
function shift(args: string[], expected: string): string {
|
|
232
|
+
export function shift(args: string[], expected: string): string {
|
|
228
233
|
try {
|
|
229
234
|
return shiftRaw(args, expected);
|
|
230
235
|
} catch (error) {
|
|
@@ -307,7 +312,10 @@ function parseJson(value: string | undefined): unknown {
|
|
|
307
312
|
}
|
|
308
313
|
}
|
|
309
314
|
|
|
310
|
-
function assertNoUnknownFlags(
|
|
315
|
+
export function assertNoUnknownFlags(
|
|
316
|
+
args: readonly string[],
|
|
317
|
+
command: string,
|
|
318
|
+
): void {
|
|
311
319
|
try {
|
|
312
320
|
assertNoUnknownFlagsRaw(args, command);
|
|
313
321
|
} catch (error) {
|
|
@@ -652,7 +660,7 @@ async function assertKnownDataKeys(
|
|
|
652
660
|
);
|
|
653
661
|
}
|
|
654
662
|
|
|
655
|
-
async function callQuery(
|
|
663
|
+
export async function callQuery(
|
|
656
664
|
ctx: CrmCliContext,
|
|
657
665
|
fn: Parameters<ConvexHttpClient["query"]>[0],
|
|
658
666
|
args: Record<string, unknown> = {},
|
|
@@ -667,7 +675,7 @@ async function callQuery(
|
|
|
667
675
|
}
|
|
668
676
|
}
|
|
669
677
|
|
|
670
|
-
async function callMutation(
|
|
678
|
+
export async function callMutation(
|
|
671
679
|
ctx: CrmCliContext,
|
|
672
680
|
fn: Parameters<ConvexHttpClient["mutation"]>[0],
|
|
673
681
|
args: Record<string, unknown> = {},
|
|
@@ -720,7 +728,7 @@ async function suppressForwardedConvexLogs<T>(
|
|
|
720
728
|
}
|
|
721
729
|
}
|
|
722
730
|
|
|
723
|
-
function out(ctx: CrmCliContext, value: unknown): void {
|
|
731
|
+
export function out(ctx: CrmCliContext, value: unknown): void {
|
|
724
732
|
if (ctx.jsonOutput) {
|
|
725
733
|
console.log(JSON.stringify(value, null, 2));
|
|
726
734
|
return;
|
|
@@ -983,6 +991,12 @@ export async function runCrmCommand(opts: {
|
|
|
983
991
|
return await runEntriesCommand(ctx);
|
|
984
992
|
case "history":
|
|
985
993
|
return await runHistoryCommand(ctx);
|
|
994
|
+
case "comments":
|
|
995
|
+
return await runCommentsCommand(ctx);
|
|
996
|
+
case "watch":
|
|
997
|
+
return await runWatchCommand(ctx);
|
|
998
|
+
case "tasks":
|
|
999
|
+
return await runTasksCommand(ctx);
|
|
986
1000
|
case "cells":
|
|
987
1001
|
return await runCellsCommand(ctx);
|
|
988
1002
|
case "query":
|
|
@@ -1045,6 +1059,9 @@ const CRM_SUBCOMMANDS = [
|
|
|
1045
1059
|
"fields",
|
|
1046
1060
|
"entries",
|
|
1047
1061
|
"history",
|
|
1062
|
+
"comments",
|
|
1063
|
+
"watch",
|
|
1064
|
+
"tasks",
|
|
1048
1065
|
"cells",
|
|
1049
1066
|
"query",
|
|
1050
1067
|
"sql",
|
|
@@ -4256,6 +4273,28 @@ Entries (rows):
|
|
|
4256
4273
|
dench crm entries list <object> [--limit N] [--with-meta] [--json]
|
|
4257
4274
|
dench crm entries get <object> <entryId>
|
|
4258
4275
|
dench crm history <object> <entryId> [--include-system] [--limit N] [--cursor CURSOR] [--json]
|
|
4276
|
+
|
|
4277
|
+
Comments (the record's Activity feed):
|
|
4278
|
+
dench crm comments list <object> <entryId> [--limit N] [--cursor CURSOR] [--replies N] [--json]
|
|
4279
|
+
dench crm comments replies <commentId> [--limit N] [--cursor CURSOR]
|
|
4280
|
+
dench crm comments get <commentId>
|
|
4281
|
+
dench crm comments create <object> <entryId> --body "markdown" [--body-file f.md] [--reply-to <commentId>] [--mention <userId>]... [--dench]
|
|
4282
|
+
Mention a member inline as [@Name](/?member=<userId>); --dench appends
|
|
4283
|
+
[@Dench](/?member=dench) so the agent replies in the thread. Returns
|
|
4284
|
+
{ id, denchRunId? } immediately — poll 'comments list' for the reply.
|
|
4285
|
+
dench crm comments update <commentId> --body "markdown"
|
|
4286
|
+
dench crm comments delete <commentId>
|
|
4287
|
+
dench crm comments react <commentId> <emoji> (toggles)
|
|
4288
|
+
dench crm watch <object> <entryId> [--off] (follow / unfollow activity)
|
|
4289
|
+
dench crm watch status <object> <entryId>
|
|
4290
|
+
|
|
4291
|
+
Tasks (shortcuts over the protected 'task' object):
|
|
4292
|
+
dench crm tasks mine [--limit N]
|
|
4293
|
+
dench crm tasks list <object> <entryId>
|
|
4294
|
+
dench crm tasks create <object> <entryId> --name "…" [--assignee <userId>] [--due YYYY-MM-DD]
|
|
4295
|
+
dench crm tasks complete <taskEntryId>
|
|
4296
|
+
dench crm tasks reopen <taskEntryId>
|
|
4297
|
+
|
|
4259
4298
|
dench crm entries create <object> --data '{"Field":"Value",...}' (alias: --fields) [--allow-unknown] [--idempotency-key <key>]
|
|
4260
4299
|
--idempotency-key upserts on (object, key): a retried create returns
|
|
4261
4300
|
the existing row with alreadyExisted: true instead of duplicating it.
|
package/dench.ts
CHANGED
|
@@ -517,9 +517,18 @@ CRM (Daytona-native rewrite):
|
|
|
517
517
|
dench crm docs <list|create|link>
|
|
518
518
|
dench crm actions <list|run> ...
|
|
519
519
|
dench crm batch --file ops.jsonl
|
|
520
|
+
dench crm comments <list|get|replies|create|update|delete|react> ... (record Activity feed; --dench summons the agent)
|
|
521
|
+
dench crm watch <object> <entryId> [--off] (follow a record's activity)
|
|
522
|
+
dench crm tasks <mine|list|create|complete|reopen> ...
|
|
520
523
|
Batch write helpers: entries create-many/update-many, cells set-many
|
|
521
524
|
Help: dench crm help
|
|
522
525
|
|
|
526
|
+
Notifications (the bell's inbox):
|
|
527
|
+
dench notifications list [--filter inbox|unread|archived|all] [--limit N] [--json]
|
|
528
|
+
dench notifications count | read <id> [--unread] | read-all | archive <id>
|
|
529
|
+
dench notifications prefs [--set emailEnabled=false] [--set <kind>.email=false]
|
|
530
|
+
Help: dench notifications help
|
|
531
|
+
|
|
523
532
|
Workspace members (for CRM \`user\`-type fields):
|
|
524
533
|
dench members list [--json] (top-level alias)
|
|
525
534
|
dench crm members list [--json] (canonical CRM-scoped name)
|
|
@@ -4569,6 +4578,41 @@ async function main() {
|
|
|
4569
4578
|
return;
|
|
4570
4579
|
}
|
|
4571
4580
|
|
|
4581
|
+
if (command === "notifications") {
|
|
4582
|
+
// The bell's inbox from the terminal. Same auth shape as `dench crm`:
|
|
4583
|
+
// agent sessions read their owner's inbox; org API keys are refused
|
|
4584
|
+
// server-side because there is no person behind them.
|
|
4585
|
+
const subArgs = stripRuntimeFlags(
|
|
4586
|
+
args.slice(args.indexOf("notifications") + 1),
|
|
4587
|
+
);
|
|
4588
|
+
const sub = subArgs[0];
|
|
4589
|
+
const { runNotificationsCommand } = await import("./notifications");
|
|
4590
|
+
if (!sub || sub === "help" || sub === "--help") {
|
|
4591
|
+
// biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
|
|
4592
|
+
await runNotificationsCommand({ convex: {} as any, args: subArgs });
|
|
4593
|
+
return;
|
|
4594
|
+
}
|
|
4595
|
+
const runtime = await getRuntime();
|
|
4596
|
+
if (runtime.mode === "dev") {
|
|
4597
|
+
await runNotificationsCommand({
|
|
4598
|
+
convex: runtime.client,
|
|
4599
|
+
args: subArgs,
|
|
4600
|
+
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
4601
|
+
});
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
4605
|
+
runtime,
|
|
4606
|
+
"dench notifications",
|
|
4607
|
+
);
|
|
4608
|
+
await runNotificationsCommand({
|
|
4609
|
+
convex: authedRuntime.client,
|
|
4610
|
+
args: subArgs,
|
|
4611
|
+
sessionToken: authedRuntime.sessionToken,
|
|
4612
|
+
});
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4572
4616
|
if (command === "fs") {
|
|
4573
4617
|
await runFsCommand();
|
|
4574
4618
|
return;
|