@dench.com/cli 0.4.8 → 0.4.9

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/members.ts ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * `dench members <subcommand>` — workspace member roster for CRM
3
+ * `user`-type fields (and anyone else who needs to look up who is in
4
+ * the org from the command line).
5
+ *
6
+ * Why this exists:
7
+ *
8
+ * CRM `user`-typed cells store a Convex `users` doc id. The UI
9
+ * resolves that id to a real avatar + name against the org's member
10
+ * roster, which the browser SPA fetches via
11
+ * `api.functions.organizations.listMembers`. That query is wired to
12
+ * `getAuthUserId(ctx)` (Convex Auth cookies), so it's invisible to
13
+ * the dench-cli running in a sandbox under `DENCH_API_KEY` or a
14
+ * `dch_agent_*` session token from `dench login`.
15
+ *
16
+ * Before this command existed, the AI agent had no way to discover
17
+ * userIds and would write display names as strings into user-typed
18
+ * cells, which the UI then rendered as unlinked badges. This module
19
+ * pairs with the new `crm/members.list` Convex query (which
20
+ * authenticates via `requireCrmAccess` and accepts every CLI bearer)
21
+ * to give the AI a way to look up `{ userId, name, email, role }` for
22
+ * every member of the org and assign cells correctly.
23
+ *
24
+ * Surfaces:
25
+ * dench members list (top-level, this module)
26
+ * dench crm members list (alias dispatched from cli/crm.ts so the
27
+ * command is also discoverable from
28
+ * inside the `dench crm` namespace, which
29
+ * is what the seed skill points the AI
30
+ * at most of the time)
31
+ *
32
+ * Both surfaces share this implementation so the CLI auth + payload
33
+ * shape stays in lockstep.
34
+ */
35
+ import { ConvexHttpClient } from "convex/browser";
36
+ import { makeFunctionReference } from "convex/server";
37
+ import { CliArgError, hasFlag } from "./lib/cli-args";
38
+
39
+ class MembersCliError extends Error {}
40
+
41
+ type MembersCliContext = {
42
+ convex: ConvexHttpClient;
43
+ args: string[];
44
+ jsonOutput: boolean;
45
+ /**
46
+ * Bearer forwarded with every Convex call so the server-side
47
+ * `requireCrmAccess` helper can resolve the caller's organization
48
+ * without a Convex Auth cookie. Two accepted formats:
49
+ * • `dch_agent_*` agent session token minted by `dench login`.
50
+ * • Unified Dench API key (`dench_pk_*`, `dench_live_*`, etc.)
51
+ * baked into sandbox envVars as `DENCH_API_KEY`.
52
+ */
53
+ sessionToken?: string;
54
+ };
55
+
56
+ type MemberRow = {
57
+ userId: string;
58
+ name: string | null;
59
+ email: string | null;
60
+ image: string | null;
61
+ role: "ADMIN" | "MEMBER";
62
+ joinedAt: number;
63
+ };
64
+
65
+ type ListMembersResult = {
66
+ members: MemberRow[];
67
+ organizationId: string;
68
+ organizationSlug: string | null;
69
+ organizationName: string;
70
+ };
71
+
72
+ const api = {
73
+ members: {
74
+ list: makeFunctionReference<"query">("functions/crm/members:list"),
75
+ },
76
+ };
77
+
78
+ export async function runMembersCommand(opts: {
79
+ convex: ConvexHttpClient;
80
+ args: string[];
81
+ sessionToken?: string;
82
+ }): Promise<void> {
83
+ const args = [...opts.args];
84
+ const jsonOutput = hasFlag(args, "--json");
85
+ const ctx: MembersCliContext = {
86
+ convex: opts.convex,
87
+ args,
88
+ jsonOutput,
89
+ sessionToken: opts.sessionToken,
90
+ };
91
+ const subcommand = args.shift();
92
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
93
+ membersHelp();
94
+ return;
95
+ }
96
+ switch (subcommand) {
97
+ case "list":
98
+ return await runListMembers(ctx);
99
+ default:
100
+ throw new MembersCliError(
101
+ `Unknown members subcommand: ${subcommand}. Run \`dench members help\`.`,
102
+ );
103
+ }
104
+ }
105
+
106
+ async function runListMembers(ctx: MembersCliContext): Promise<void> {
107
+ if (ctx.args.some((arg) => arg.startsWith("--"))) {
108
+ // No subflags supported beyond --json (which hasFlag already drained).
109
+ // Surface a typo with a useful hint so the AI doesn't silently get a
110
+ // full list when it asked for a filter that doesn't exist.
111
+ throw new MembersCliError(
112
+ `Unknown flag: ${ctx.args.find((arg) => arg.startsWith("--"))}. \`dench members list\` only accepts --json.`,
113
+ );
114
+ }
115
+ const result = (await callQuery(ctx, api.members.list, {})) as
116
+ | ListMembersResult
117
+ | null
118
+ | undefined;
119
+ if (ctx.jsonOutput) {
120
+ console.log(JSON.stringify(result ?? { members: [] }, null, 2));
121
+ return;
122
+ }
123
+ const members = result?.members ?? [];
124
+ if (members.length === 0) {
125
+ console.log("(no members)");
126
+ return;
127
+ }
128
+ // Plain table for human consumption — userIds are first column so the
129
+ // operator can copy/paste directly into a `dench crm cells set ...
130
+ // --field Owner --value <userId>` follow-up.
131
+ const headers = ["userId", "role", "name", "email"];
132
+ const rows = members.map((m) => [
133
+ m.userId,
134
+ m.role,
135
+ m.name ?? "",
136
+ m.email ?? "",
137
+ ]);
138
+ const widths = headers.map((header, idx) =>
139
+ Math.max(header.length, ...rows.map((row) => row[idx].length)),
140
+ );
141
+ const formatRow = (row: string[]) =>
142
+ row.map((cell, idx) => cell.padEnd(widths[idx])).join(" ");
143
+ console.log(formatRow(headers));
144
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
145
+ for (const row of rows) {
146
+ console.log(formatRow(row));
147
+ }
148
+ }
149
+
150
+ async function callQuery(
151
+ ctx: MembersCliContext,
152
+ fn: Parameters<ConvexHttpClient["query"]>[0],
153
+ args: Record<string, unknown> = {},
154
+ ): Promise<unknown> {
155
+ try {
156
+ return await ctx.convex.query(fn, {
157
+ ...args,
158
+ ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
159
+ } as never);
160
+ } catch (error) {
161
+ if (error instanceof CliArgError) throw new MembersCliError(error.message);
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ function membersHelp(): void {
167
+ console.log(`Usage: dench members <subcommand>
168
+
169
+ Workspace members for the active organization. Use these userIds when
170
+ assigning CRM \`user\`-type cells (e.g. \`Owner\`, \`Assignee\`) so the
171
+ UI shows a real linked avatar instead of an unlinked literal string.
172
+
173
+ Subcommands:
174
+ dench members list [--json]
175
+ List every member of the current org as \`userId / role / name / email\`.
176
+ Pass --json for a machine-readable shape that includes
177
+ organizationId / organizationSlug / organizationName.
178
+
179
+ Alias:
180
+ dench crm members list [--json]
181
+ Same surface, dispatched from inside the \`dench crm\` namespace
182
+ since the canonical use case is assigning CRM \`user\` fields.
183
+
184
+ After picking a userId:
185
+ dench crm cells set task --entry <entryId> --field Owner --value <userId>
186
+ dench crm entries create task --data '{"Name":"Ship CRM","Owner":"<userId>"}'
187
+
188
+ The server best-effort-resolves a name / email passed to a \`user\`
189
+ field when the match against the roster is unique, but the explicit
190
+ lookup via \`dench members list\` is the correct pattern.
191
+ `);
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,11 +24,13 @@
24
24
  "chat.ts",
25
25
  "chat-spawn.ts",
26
26
  "crm.ts",
27
+ "members.ts",
27
28
  "cron.ts",
28
29
  "search.ts",
29
30
  "image.ts",
30
31
  "tools.ts",
31
32
  "agentKind.ts",
33
+ "agent-config.ts",
32
34
  "host.ts",
33
35
  "openUrl.ts",
34
36
  "session.ts",