@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.
@@ -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(args: readonly string[], command: string): void {
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;