@dench.com/cli 2.1.5 → 2.2.1

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.
@@ -1068,6 +1068,65 @@ const rawApiOperations = [
1068
1068
  responseSchema: anyResponse,
1069
1069
  backend: gateway("POST", "/enrichment/company/search"),
1070
1070
  }),
1071
+ op({
1072
+ id: "crm.templates.list",
1073
+ group: "crm",
1074
+ summary:
1075
+ "List one-click CRM templates (objects + automations + sample rows) with per-workspace installed flags.",
1076
+ cli: "dench crm templates list",
1077
+ method: "GET",
1078
+ path: "/crm/templates",
1079
+ requestSchema: noBody,
1080
+ responseSchema: anyResponse,
1081
+ backend: convex("query", "functions/crm/templates:listAvailable"),
1082
+ }),
1083
+ op({
1084
+ id: "crm.templates.install",
1085
+ group: "crm",
1086
+ summary:
1087
+ "Install a CRM template. Idempotent: re-installs top up missing pieces and never duplicate objects, automations, or sample rows.",
1088
+ cli: "dench crm templates install",
1089
+ method: "POST",
1090
+ path: "/crm/templates/{templateId}/install",
1091
+ requestSchema: noBody,
1092
+ responseSchema: anyResponse,
1093
+ backend: convex("mutation", "functions/crm/templates:install"),
1094
+ }),
1095
+ op({
1096
+ id: "crm.templates.removeSampleData",
1097
+ group: "crm",
1098
+ summary:
1099
+ "Delete a CRM template's sample rows (tagged \"Sample\"); schema, views, and automations stay.",
1100
+ cli: "dench crm templates remove-sample-data",
1101
+ method: "POST",
1102
+ path: "/crm/templates/{templateId}/remove-sample-data",
1103
+ requestSchema: noBody,
1104
+ responseSchema: anyResponse,
1105
+ backend: convex("mutation", "functions/crm/templates:removeSampleData"),
1106
+ }),
1107
+ op({
1108
+ id: "crm.automations.list",
1109
+ group: "crm",
1110
+ summary:
1111
+ "List deterministic CRM automations (rules that fire inside entry writes, e.g. qualified prospect → opportunity).",
1112
+ cli: "dench crm automations list",
1113
+ method: "GET",
1114
+ path: "/crm/automations",
1115
+ requestSchema: noBody,
1116
+ responseSchema: anyResponse,
1117
+ backend: convex("query", "functions/crm/automations:list"),
1118
+ }),
1119
+ op({
1120
+ id: "crm.automations.setEnabled",
1121
+ group: "crm",
1122
+ summary: "Enable or disable a CRM automation.",
1123
+ cli: "dench crm automations enable|disable",
1124
+ method: "PATCH",
1125
+ path: "/crm/automations/{automationId}",
1126
+ requestSchema: anyObject,
1127
+ responseSchema: anyResponse,
1128
+ backend: convex("mutation", "functions/crm/automations:setEnabled"),
1129
+ }),
1071
1130
  op({
1072
1131
  id: "crm.import",
1073
1132
  group: "local",
@@ -1158,6 +1217,89 @@ const rawApiOperations = [
1158
1217
  responseSchema: anyResponse,
1159
1218
  backend: gateway("GET", "/email/identities/{identity}/status"),
1160
1219
  }),
1220
+ op({
1221
+ id: "email.identity.list",
1222
+ group: "email",
1223
+ summary: "List the workspace's email sending identities.",
1224
+ cli: "dench email identity list",
1225
+ method: "GET",
1226
+ path: "/email/sending-identities",
1227
+ requestSchema: anyObject,
1228
+ responseSchema: anyResponse,
1229
+ backend: convex("query", "functions/emailCampaigns:listIdentities"),
1230
+ }),
1231
+ op({
1232
+ id: "email.identity.dns",
1233
+ group: "email",
1234
+ summary:
1235
+ "Fetch DNS records (DKIM/SPF/DMARC) for a pending domain sender verification.",
1236
+ cli: "dench email identity dns",
1237
+ method: "GET",
1238
+ path: "/email/sending-identities/{identityId}/dns",
1239
+ requestSchema: anyObject,
1240
+ responseSchema: anyResponse,
1241
+ backend: convex(
1242
+ "action",
1243
+ "functions/emailCampaignsNode:refreshIdentityStatus",
1244
+ ),
1245
+ }),
1246
+ op({
1247
+ id: "email.identity.disable",
1248
+ group: "email",
1249
+ summary:
1250
+ "Soft-disable a sender (or every sender on a domain) for the workspace; sends are blocked until restored.",
1251
+ cli: "dench email identity disable",
1252
+ method: "POST",
1253
+ path: "/email/sending-identities/{identity}/disable",
1254
+ requestSchema: anyObject,
1255
+ responseSchema: anyResponse,
1256
+ backend: convex("mutation", "functions/emailCampaigns:disableIdentity"),
1257
+ }),
1258
+ op({
1259
+ id: "email.identity.restore",
1260
+ group: "email",
1261
+ summary: "Restore a previously disabled sender for the workspace.",
1262
+ cli: "dench email identity restore",
1263
+ method: "POST",
1264
+ path: "/email/sending-identities/{identity}/restore",
1265
+ requestSchema: anyObject,
1266
+ responseSchema: anyResponse,
1267
+ backend: convex("mutation", "functions/emailCampaigns:restoreIdentity"),
1268
+ }),
1269
+ op({
1270
+ id: "email.send",
1271
+ group: "email",
1272
+ summary:
1273
+ "Send one raw email (no campaign): to/cc/bcc, custom from-name and reply-to, optional reply threading via replyToMessageId.",
1274
+ cli: "dench email send",
1275
+ method: "POST",
1276
+ path: "/email/send",
1277
+ requestSchema: anyObject,
1278
+ responseSchema: anyResponse,
1279
+ backend: convex("action", "functions/emailCampaignsNode:sendEmail"),
1280
+ }),
1281
+ op({
1282
+ id: "email.message.get",
1283
+ group: "email",
1284
+ summary: "Read a raw email message's status and open/click counters.",
1285
+ cli: "dench email message status",
1286
+ method: "GET",
1287
+ path: "/email/messages/{messageId}",
1288
+ requestSchema: anyObject,
1289
+ responseSchema: anyResponse,
1290
+ backend: convex("query", "functions/emailCampaigns:getMessage"),
1291
+ }),
1292
+ op({
1293
+ id: "email.message.list",
1294
+ group: "email",
1295
+ summary: "List recent raw email messages for the workspace.",
1296
+ cli: "dench email message list",
1297
+ method: "GET",
1298
+ path: "/email/messages",
1299
+ requestSchema: anyObject,
1300
+ responseSchema: anyResponse,
1301
+ backend: convex("query", "functions/emailCampaigns:listMessages"),
1302
+ }),
1161
1303
  op({
1162
1304
  id: "email.template.create",
1163
1305
  group: "email",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.1.5",
3
+ "version": "2.2.1",
4
4
  "description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,6 @@
28
28
  "members.ts",
29
29
  "cron.ts",
30
30
  "browser.ts",
31
- "linkedin.ts",
32
31
  "search.ts",
33
32
  "image.ts",
34
33
  "tools.ts",
package/linkedin.ts DELETED
@@ -1,210 +0,0 @@
1
- /**
2
- * `dench linkedin` — first-class LinkedIn outreach workflows.
3
- *
4
- * This is intentionally not a generic browser surface. The CLI sends
5
- * one high-level command to Dench Cloud, where Browserbase + the
6
- * LinkedIn-specific verifier do the whole transaction server-side.
7
- */
8
-
9
- import { hasFlag } from "./lib/cli-args";
10
-
11
- type RuntimeBundle = {
12
- host: string;
13
- bearerToken: string;
14
- };
15
-
16
- type LinkedInCliContext = {
17
- args: string[];
18
- jsonOutput: boolean;
19
- runtime: RuntimeBundle;
20
- };
21
-
22
- type LinkedInResponse = Record<string, unknown> & {
23
- crmUpdate?: unknown;
24
- currentUrl?: string | null;
25
- error?: string;
26
- message?: string;
27
- ok?: boolean;
28
- pageTitle?: string | null;
29
- profileUrl?: string;
30
- status?: string;
31
- threadId?: string;
32
- threadUrl?: string;
33
- verified?: boolean;
34
- };
35
-
36
- class LinkedInCliError extends Error {}
37
-
38
- function linkedinHelp(): void {
39
- console.log(`Usage: dench linkedin <subcommand>
40
-
41
- Connect with a LinkedIn profile using the cloud Browserbase browser:
42
- dench linkedin connect <profileUrl> [--entry people:<entryId>] [--thread <threadId>] [--json]
43
-
44
- CRM:
45
- --entry people:<entryId> Update that CRM row after verification.
46
- --object <object> --entry-id <entryId>
47
- Same as --entry, split into two flags.
48
-
49
- Behavior:
50
- CRM is marked reached out only when LinkedIn verifies Pending.
51
- If LinkedIn asks for login, rate-limits, or shows an unclear modal,
52
- the result is manual_review and the browser thread stays live.
53
- `);
54
- }
55
-
56
- function consumeFlagValue(args: string[], name: string): string | undefined {
57
- const idx = args.indexOf(name);
58
- if (idx === -1) return undefined;
59
- const value = args[idx + 1];
60
- args.splice(idx, 2);
61
- return value;
62
- }
63
-
64
- function normalizeProfileUrl(input: string | undefined): string {
65
- const value = input?.trim();
66
- if (!value) {
67
- throw new LinkedInCliError(
68
- "Usage: dench linkedin connect <profileUrl> [--entry people:<entryId>]",
69
- );
70
- }
71
- if (/^https?:\/\//i.test(value)) return value;
72
- return `https://${value}`;
73
- }
74
-
75
- function parseEntry(value: string | undefined) {
76
- if (!value) return null;
77
- const [objectName, ...rest] = value.split(":");
78
- const entryId = rest.join(":").trim();
79
- if (!objectName?.trim() || !entryId) {
80
- throw new LinkedInCliError("--entry must look like people:<entryId>");
81
- }
82
- return { objectName: objectName.trim(), entryId };
83
- }
84
-
85
- function buildLinkedInCliUrl(host: string): string {
86
- return `${host.replace(/\/+$/, "")}/api/linkedin/cli`;
87
- }
88
-
89
- async function postJson(
90
- url: string,
91
- bearer: string,
92
- body: unknown,
93
- ): Promise<{ ok: boolean; payload: unknown; status: number }> {
94
- const response = await fetch(url, {
95
- method: "POST",
96
- headers: {
97
- accept: "application/json",
98
- authorization: `Bearer ${bearer}`,
99
- "content-type": "application/json",
100
- },
101
- body: JSON.stringify(body),
102
- });
103
- const text = await response.text();
104
- let payload: unknown;
105
- try {
106
- payload = text ? JSON.parse(text) : null;
107
- } catch {
108
- payload = { raw: text };
109
- }
110
- return { ok: response.ok, payload, status: response.status };
111
- }
112
-
113
- function payloadError(payload: unknown): string {
114
- if (payload && typeof payload === "object" && !Array.isArray(payload)) {
115
- const error = (payload as { error?: unknown }).error;
116
- if (typeof error === "string" && error.trim()) return error;
117
- }
118
- return "LinkedIn request failed";
119
- }
120
-
121
- function output(ctx: LinkedInCliContext, value: unknown): void {
122
- if (ctx.jsonOutput) {
123
- console.log(JSON.stringify(value, null, 2));
124
- return;
125
- }
126
- console.log(String(value));
127
- }
128
-
129
- function formatHuman(response: LinkedInResponse) {
130
- const lines: string[] = [];
131
- const status = response.status ?? "unknown";
132
- if (response.ok === false) {
133
- lines.push(`LinkedIn connect failed: ${response.error ?? status}`);
134
- } else {
135
- lines.push(`LinkedIn connect: ${status}`);
136
- }
137
- if (response.verified !== undefined) {
138
- lines.push(`verified: ${response.verified ? "yes" : "no"}`);
139
- }
140
- if (response.message) lines.push(`result: ${response.message}`);
141
- if (response.profileUrl) lines.push(`profile: ${response.profileUrl}`);
142
- if (response.currentUrl) lines.push(`url: ${response.currentUrl}`);
143
- if (response.threadId) lines.push(`threadId: ${response.threadId}`);
144
- if (response.crmUpdate) lines.push("crm: updated");
145
- if (response.threadUrl) {
146
- lines.push("");
147
- lines.push(`Open in Dench: ${response.threadUrl}`);
148
- }
149
- return lines.join("\n");
150
- }
151
-
152
- async function runConnectCommand(ctx: LinkedInCliContext) {
153
- const threadId = consumeFlagValue(ctx.args, "--thread");
154
- const entry = parseEntry(consumeFlagValue(ctx.args, "--entry"));
155
- const objectName =
156
- consumeFlagValue(ctx.args, "--object") ?? entry?.objectName;
157
- const entryId = consumeFlagValue(ctx.args, "--entry-id") ?? entry?.entryId;
158
- const profileUrl = normalizeProfileUrl(ctx.args.shift());
159
-
160
- const body = {
161
- action: "connect",
162
- profileUrl,
163
- threadId,
164
- ...(objectName ? { crmObjectName: objectName } : {}),
165
- ...(entryId ? { crmEntryId: entryId } : {}),
166
- };
167
-
168
- const result = await postJson(
169
- buildLinkedInCliUrl(ctx.runtime.host),
170
- ctx.runtime.bearerToken,
171
- body,
172
- );
173
- if (!result.ok) {
174
- throw new LinkedInCliError(
175
- `${payloadError(result.payload)} (${result.status})`,
176
- );
177
- }
178
-
179
- const response =
180
- result.payload && typeof result.payload === "object"
181
- ? (result.payload as LinkedInResponse)
182
- : ({} as LinkedInResponse);
183
-
184
- output(ctx, ctx.jsonOutput ? response : formatHuman(response));
185
- }
186
-
187
- export async function runLinkedInCommand(opts: {
188
- args: string[];
189
- runtime: RuntimeBundle;
190
- }): Promise<void> {
191
- const args = [...opts.args];
192
- const jsonOutput = hasFlag(args, "--json");
193
- const ctx: LinkedInCliContext = {
194
- args,
195
- jsonOutput,
196
- runtime: opts.runtime,
197
- };
198
- const subcommand = args.shift();
199
- if (!subcommand || subcommand === "help" || subcommand === "--help") {
200
- linkedinHelp();
201
- return;
202
- }
203
- if (subcommand === "connect") {
204
- await runConnectCommand(ctx);
205
- return;
206
- }
207
- throw new LinkedInCliError(`Unknown linkedin subcommand: ${subcommand}`);
208
- }
209
-
210
- export { LinkedInCliError };