@dench.com/cli 3.5.6-staging.344 → 3.6.0-staging.356

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,189 @@
1
+ export type TimelineRun = {
2
+ _id: string;
3
+ stepId: string;
4
+ stepIndex: number;
5
+ type: string;
6
+ status: string;
7
+ scheduledAt?: number;
8
+ startedAt?: number;
9
+ waitUntil?: number;
10
+ retryAt?: number;
11
+ completedAt?: number;
12
+ emailMessageId?: string;
13
+ lastError?: string;
14
+ result?: unknown;
15
+ };
16
+
17
+ export type TimelineEmail = {
18
+ _id: string;
19
+ subject?: string;
20
+ textBody?: string;
21
+ status?: string;
22
+ sentAt?: number;
23
+ firstOpenedAt?: number;
24
+ firstClickedAt?: number;
25
+ lastError?: string;
26
+ };
27
+
28
+ export type TimelineLinkedin = {
29
+ outreachStepRunId?: string;
30
+ direction?: string;
31
+ text?: string;
32
+ sentAt?: number;
33
+ deliveredAt?: number;
34
+ readAt?: number;
35
+ };
36
+
37
+ export type TimelineResult = {
38
+ enrollment: {
39
+ _id: string;
40
+ displayName?: string;
41
+ status: string;
42
+ lastError?: string;
43
+ repliedAt?: number;
44
+ linkedinProfileUrl?: string;
45
+ personalization?: unknown;
46
+ };
47
+ steps: Array<{
48
+ _id: string;
49
+ type: string;
50
+ order: number;
51
+ waitHours?: number;
52
+ }>;
53
+ runs: TimelineRun[];
54
+ emailMessages: TimelineEmail[];
55
+ linkedinMessages: TimelineLinkedin[];
56
+ };
57
+
58
+ export type TimelineStep = {
59
+ step: number;
60
+ stepId: string;
61
+ type: string;
62
+ status: string;
63
+ outcome: string;
64
+ at: number | null;
65
+ detail: string[];
66
+ subject?: string;
67
+ text?: string;
68
+ error?: string;
69
+ };
70
+
71
+ const SKIP_REASONS: Record<string, string> = {
72
+ missing_email_identity: "Skipped: no email address",
73
+ missing_linkedin_identity: "Skipped: no LinkedIn profile",
74
+ not_connected_on_linkedin: "Skipped: not connected on LinkedIn",
75
+ already_connected_on_linkedin: "Skipped: already connected on LinkedIn",
76
+ already_connected: "Skipped: already connected",
77
+ not_connected: "Skipped: not connected on LinkedIn",
78
+ linkedin_connection_timeout:
79
+ "Skipped: they did not accept the request in time",
80
+ recipient_invited_sender: "Skipped: they had already sent you a request",
81
+ provider_outcome_unresolved: "Skipped: we could not confirm it went out",
82
+ unsubscribe: "Skipped: they unsubscribed",
83
+ bounce: "Skipped: their address bounced",
84
+ };
85
+
86
+ const RUN_STATUS: Record<string, string> = {
87
+ queued: "Queued",
88
+ running: "Sending",
89
+ waiting: "Waiting",
90
+ retry: "Will retry",
91
+ sent: "Done",
92
+ skipped: "Skipped",
93
+ failed: "Failed",
94
+ unknown: "Needs a look",
95
+ };
96
+
97
+ const EMAIL_STATUS: Record<string, string> = {
98
+ queued: "sending",
99
+ bounced: "bounced",
100
+ complained: "marked as spam",
101
+ failed: "could not send",
102
+ };
103
+
104
+ function skipReason(result: unknown): string {
105
+ const reason =
106
+ result && typeof result === "object"
107
+ ? (result as { reason?: unknown }).reason
108
+ : undefined;
109
+ return (typeof reason === "string" && SKIP_REASONS[reason]) || "Skipped";
110
+ }
111
+
112
+ function outcomeOf(run: TimelineRun): string {
113
+ if (run.status === "skipped") return skipReason(run.result);
114
+ if (run.status === "sent") {
115
+ if (run.type === "wait") return "Waited";
116
+ if (run.type === "linkedin_visit") return "Visited";
117
+ return "Sent";
118
+ }
119
+ return RUN_STATUS[run.status] ?? run.status;
120
+ }
121
+
122
+ export function timelineSteps(result: TimelineResult): TimelineStep[] {
123
+ const steps = new Map(result.steps.map((step) => [String(step._id), step]));
124
+ const emails = new Map(
125
+ result.emailMessages.map((message) => [String(message._id), message]),
126
+ );
127
+ const linkedin = new Map(
128
+ result.linkedinMessages.map((message) => [
129
+ String(message.outreachStepRunId ?? ""),
130
+ message,
131
+ ]),
132
+ );
133
+ return [...result.runs]
134
+ .sort((a, b) => a.stepIndex - b.stepIndex)
135
+ .map((run) => {
136
+ const step = steps.get(String(run.stepId));
137
+ const email = run.emailMessageId
138
+ ? emails.get(String(run.emailMessageId))
139
+ : undefined;
140
+ const message = linkedin.get(String(run._id));
141
+ const detail: string[] = [];
142
+ if (email) {
143
+ if (email.firstClickedAt) detail.push("clicked");
144
+ else if (email.firstOpenedAt) detail.push("opened");
145
+ else if (email.status && EMAIL_STATUS[email.status])
146
+ detail.push(EMAIL_STATUS[email.status]);
147
+ } else if (message) {
148
+ if (message.readAt) detail.push("read");
149
+ else if (message.deliveredAt) detail.push("delivered");
150
+ }
151
+ const text = (email?.textBody ?? message?.text)?.trim();
152
+ const problem =
153
+ run.status === "sent" || run.status === "skipped"
154
+ ? undefined
155
+ : (run.lastError ?? email?.lastError)?.trim();
156
+ return {
157
+ step: (step?.order ?? run.stepIndex) + 1,
158
+ stepId: String(run.stepId),
159
+ type: run.type,
160
+ status: run.status,
161
+ outcome: outcomeOf(run),
162
+ at:
163
+ run.completedAt ??
164
+ run.retryAt ??
165
+ run.waitUntil ??
166
+ run.startedAt ??
167
+ run.scheduledAt ??
168
+ null,
169
+ detail,
170
+ ...(email?.subject ? { subject: email.subject } : {}),
171
+ ...(text ? { text } : {}),
172
+ ...(problem ? { error: problem } : {}),
173
+ };
174
+ });
175
+ }
176
+
177
+ export function personAddress(enrollment: TimelineResult["enrollment"]) {
178
+ const snapshot =
179
+ enrollment.personalization && typeof enrollment.personalization === "object"
180
+ ? (enrollment.personalization as Record<string, unknown>)
181
+ : {};
182
+ const email = typeof snapshot.email === "string" ? snapshot.email : undefined;
183
+ return {
184
+ ...(email ? { email } : {}),
185
+ ...(enrollment.linkedinProfileUrl
186
+ ? { linkedinProfileUrl: enrollment.linkedinProfileUrl }
187
+ : {}),
188
+ };
189
+ }
@@ -0,0 +1,14 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ export async function cliVersion(): Promise<string> {
4
+ try {
5
+ const packageJson = JSON.parse(
6
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
7
+ ) as { version?: unknown };
8
+ return typeof packageJson.version === "string"
9
+ ? packageJson.version
10
+ : "unknown";
11
+ } catch {
12
+ return "unknown";
13
+ }
14
+ }