@dench.com/cli 2.1.0 → 2.1.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/browser.ts +507 -0
- package/crm.ts +291 -37
- package/dench.ts +124 -41
- package/fs-daemon.ts +201 -2
- package/lib/api-schemas.ts +1100 -0
- package/lib/command-registry.ts +1664 -0
- package/lib/enrichment-gateway.ts +22 -0
- package/lib/organizationSelector.ts +58 -0
- package/linkedin.ts +210 -0
- package/package.json +6 -2
|
@@ -1280,6 +1280,28 @@ async function buildGatewayError(
|
|
|
1280
1280
|
code: code ?? "not_found",
|
|
1281
1281
|
});
|
|
1282
1282
|
}
|
|
1283
|
+
if (
|
|
1284
|
+
response.status === 401 ||
|
|
1285
|
+
code === "invalid_api_key" ||
|
|
1286
|
+
code === "unauthorized"
|
|
1287
|
+
) {
|
|
1288
|
+
return new EnrichmentGatewayError(
|
|
1289
|
+
upstreamMessage ?? "Unauthorized gateway request",
|
|
1290
|
+
{ status: response.status, code: "unauthorized" },
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
if (response.status === 403 || code === "forbidden") {
|
|
1294
|
+
return new EnrichmentGatewayError(
|
|
1295
|
+
upstreamMessage ?? "Forbidden gateway request",
|
|
1296
|
+
{ status: response.status, code: "forbidden" },
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
if (code === "aviato_unauthorized") {
|
|
1300
|
+
return new EnrichmentGatewayError(
|
|
1301
|
+
upstreamMessage ?? "Aviato provider unauthorized",
|
|
1302
|
+
{ status: response.status || 401, code: "aviato_unauthorized" },
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1283
1305
|
if (response.status === 503 || code === "provider_unavailable") {
|
|
1284
1306
|
return new EnrichmentGatewayError("Gateway providers unavailable", {
|
|
1285
1307
|
status: response.status,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type OrganizationSelectorFields = {
|
|
2
|
+
_id?: string;
|
|
3
|
+
id?: string;
|
|
4
|
+
name: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function normalizeOrganizationSelector(input: string | undefined | null) {
|
|
9
|
+
const trimmed = input?.trim();
|
|
10
|
+
if (!trimmed) return undefined;
|
|
11
|
+
return trimmed.slice(0, 160);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function organizationId(organization: OrganizationSelectorFields) {
|
|
15
|
+
return String(organization._id ?? organization.id ?? "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function organizationMatchesSelector(
|
|
19
|
+
organization: OrganizationSelectorFields | null | undefined,
|
|
20
|
+
selector: string | null | undefined,
|
|
21
|
+
) {
|
|
22
|
+
const normalizedSelector =
|
|
23
|
+
normalizeOrganizationSelector(selector)?.toLowerCase();
|
|
24
|
+
if (!organization || !normalizedSelector) return false;
|
|
25
|
+
return (
|
|
26
|
+
organizationId(organization).toLowerCase() === normalizedSelector ||
|
|
27
|
+
organization.slug.toLowerCase() === normalizedSelector ||
|
|
28
|
+
organization.name.trim().toLowerCase() === normalizedSelector
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveOrganizationBySelector<
|
|
33
|
+
T extends OrganizationSelectorFields,
|
|
34
|
+
>(organizations: T[], selector: string | null | undefined): T | null {
|
|
35
|
+
const normalizedSelector =
|
|
36
|
+
normalizeOrganizationSelector(selector)?.toLowerCase();
|
|
37
|
+
if (!normalizedSelector) return null;
|
|
38
|
+
|
|
39
|
+
const slugMatch = organizations.find(
|
|
40
|
+
(organization) =>
|
|
41
|
+
organization.slug.toLowerCase() === normalizedSelector,
|
|
42
|
+
);
|
|
43
|
+
if (slugMatch) return slugMatch;
|
|
44
|
+
|
|
45
|
+
const idMatch = organizations.find(
|
|
46
|
+
(organization) =>
|
|
47
|
+
organizationId(organization).toLowerCase() === normalizedSelector,
|
|
48
|
+
);
|
|
49
|
+
if (idMatch) return idMatch;
|
|
50
|
+
|
|
51
|
+
const nameMatches = organizations.filter(
|
|
52
|
+
(organization) =>
|
|
53
|
+
organization.name.trim().toLowerCase() === normalizedSelector,
|
|
54
|
+
);
|
|
55
|
+
if (nameMatches.length === 1) return nameMatches[0];
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
package/linkedin.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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": {
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"crm.ts",
|
|
27
27
|
"members.ts",
|
|
28
28
|
"cron.ts",
|
|
29
|
+
"browser.ts",
|
|
30
|
+
"linkedin.ts",
|
|
29
31
|
"search.ts",
|
|
30
32
|
"image.ts",
|
|
31
33
|
"tools.ts",
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"host.ts",
|
|
35
37
|
"openUrl.ts",
|
|
36
38
|
"session.ts",
|
|
39
|
+
"lib/organizationSelector.ts",
|
|
37
40
|
"lib/",
|
|
38
41
|
"README.md"
|
|
39
42
|
],
|
|
@@ -43,7 +46,8 @@
|
|
|
43
46
|
"dependencies": {
|
|
44
47
|
"chokidar": "^5.0.0",
|
|
45
48
|
"convex": "1.34.0",
|
|
46
|
-
"tsx": "^4.21.0"
|
|
49
|
+
"tsx": "^4.21.0",
|
|
50
|
+
"zod": "4.3.6"
|
|
47
51
|
},
|
|
48
52
|
"publishConfig": {
|
|
49
53
|
"access": "public"
|