@dench.com/cli 3.5.5 → 3.5.6-staging.344
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/README.md +4 -3
- package/app-sdk.d.ts +32 -3
- package/campaign.ts +109 -18
- package/dench.ts +11 -2
- package/lib/api-schemas.ts +59 -0
- package/lib/command-registry.ts +24 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -334,8 +334,9 @@ dench campaign pause|resume|archive <campaign>
|
|
|
334
334
|
dench campaign stop <campaign> --person <enrollmentId>
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
-
|
|
338
|
-
cap and stop-on-reply stay
|
|
337
|
+
`steps set` works on drafts only; after launch, edit a step's text or wait
|
|
338
|
+
time in the Dench app. The schedule, daily cap and stop-on-reply stay
|
|
339
|
+
editable with `dench campaign set`. A campaign
|
|
339
340
|
holds at most 300 people. Every command takes `--json`. The full guide,
|
|
340
341
|
including the `steps.json` shape and the `--window` spec, is
|
|
341
342
|
`references/CAMPAIGNS.md` (`dench campaign help` for the flag list).
|
|
@@ -355,7 +356,7 @@ returned. LinkedIn summaries expose `linkedinAccountId`, display/owner labels,
|
|
|
355
356
|
availability, `remainingToday` (invites/messages/profiles) and
|
|
356
357
|
`remainingInvitesThisWeek`. Capacity is an account-local usage snapshot before
|
|
357
358
|
queued work and campaign limits; today's invites also respect the weekly cap.
|
|
358
|
-
The API equivalent is `GET /
|
|
359
|
+
The API equivalent is `GET $DENCH_API_BASE/campaigns/accounts/linkedin`.
|
|
359
360
|
|
|
360
361
|
Outreach needs a workspace member's credentials (a `dench signin` session, a
|
|
361
362
|
member-bound `DENCH_API_KEY`, or an agent session); an organization-only key
|
package/app-sdk.d.ts
CHANGED
|
@@ -1824,9 +1824,20 @@ export interface ApiRequests {
|
|
|
1824
1824
|
campaignId: string;
|
|
1825
1825
|
};
|
|
1826
1826
|
/** List managed email campaigns (paginated, optional status filter). — `dench email campaign list` — GET /api/v1/email/campaigns */
|
|
1827
|
-
"email.campaign.list":
|
|
1827
|
+
"email.campaign.list": {
|
|
1828
|
+
limit?: number;
|
|
1829
|
+
cursor?: string;
|
|
1830
|
+
status?: string;
|
|
1831
|
+
};
|
|
1828
1832
|
/** List a campaign's recipients (paginated, optional status filter). — `dench email campaign recipients list` — GET /api/v1/email/campaigns/{campaignId}/recipients */
|
|
1829
|
-
"email.campaign.recipients.list":
|
|
1833
|
+
"email.campaign.recipients.list": {
|
|
1834
|
+
campaignId: string;
|
|
1835
|
+
limit?: number;
|
|
1836
|
+
cursor?: string;
|
|
1837
|
+
status?: string;
|
|
1838
|
+
/** Default true: attach each recipient's CRM record. */
|
|
1839
|
+
includePeople?: boolean;
|
|
1840
|
+
};
|
|
1830
1841
|
/** Add campaign recipients from a CRM object: pick the object, the email field, and the entry ids. — `dench email campaign recipients add-from-object` — POST /api/v1/email/campaigns/{campaignId}/recipients/from-object */
|
|
1831
1842
|
"email.campaign.recipients.addFromObject": Record<string, unknown>;
|
|
1832
1843
|
/** Create a follow-up sequence from a connected inbox or verified sender. — `dench email sequence create` — POST /api/v1/email/sequences */
|
|
@@ -1856,7 +1867,12 @@ export interface ApiRequests {
|
|
|
1856
1867
|
/** Enroll people from a CRM object: pick the object, the email field, and the entry ids. — `dench email sequence enroll-from-object` — POST /api/v1/email/sequences/{sequenceId}/enroll/from-object */
|
|
1857
1868
|
"email.sequence.enrollFromObject": Record<string, unknown>;
|
|
1858
1869
|
/** List a sequence's enrollments (paginated, optional status filter). — `dench email sequence enrollments list` — GET /api/v1/email/sequences/{sequenceId}/enrollments */
|
|
1859
|
-
"email.sequence.enrollments.list":
|
|
1870
|
+
"email.sequence.enrollments.list": {
|
|
1871
|
+
sequenceId: string;
|
|
1872
|
+
limit?: number;
|
|
1873
|
+
cursor?: string;
|
|
1874
|
+
status?: string;
|
|
1875
|
+
};
|
|
1860
1876
|
/** Pause one enrollment. — `dench email sequence enrollment pause` — POST /api/v1/email/sequences/enrollments/{enrollmentId}/pause */
|
|
1861
1877
|
"email.sequence.enrollment.pause": Record<string, unknown>;
|
|
1862
1878
|
/** Resume one paused enrollment. — `dench email sequence enrollment resume` — POST /api/v1/email/sequences/enrollments/{enrollmentId}/resume */
|
|
@@ -1918,6 +1934,14 @@ export interface ApiRequests {
|
|
|
1918
1934
|
/** Days sends may go out: 0 = Sunday ... 6 = Saturday. */
|
|
1919
1935
|
weekdays: number[];
|
|
1920
1936
|
}[];
|
|
1937
|
+
/** Legacy single-window timezone. Prefer sendSchedules. */
|
|
1938
|
+
timezone?: string;
|
|
1939
|
+
/** Legacy single-window start hour. Prefer sendSchedules. */
|
|
1940
|
+
sendWindowStartHour?: number;
|
|
1941
|
+
/** Legacy single-window end hour. Prefer sendSchedules. */
|
|
1942
|
+
sendWindowEndHour?: number;
|
|
1943
|
+
/** Legacy single-window weekdays, 0 = Sunday. Prefer sendSchedules. */
|
|
1944
|
+
sendWeekdays?: number[];
|
|
1921
1945
|
/** Cold emails per day, 1-100. */
|
|
1922
1946
|
dailySendCap?: number;
|
|
1923
1947
|
};
|
|
@@ -1976,6 +2000,11 @@ export interface ApiRequests {
|
|
|
1976
2000
|
entryId?: string;
|
|
1977
2001
|
sourceEntryId?: string;
|
|
1978
2002
|
sourceObjectName?: string;
|
|
2003
|
+
emailIdentityId?: string;
|
|
2004
|
+
linkedinIdentityId?: string;
|
|
2005
|
+
linkedinProviderUserId?: string;
|
|
2006
|
+
/** CRM object a new record is created in when none matches. */
|
|
2007
|
+
newRecordsObject?: "lead" | "people";
|
|
1979
2008
|
personalization?: Record<string, unknown>;
|
|
1980
2009
|
checkDuplicatesAcrossWorkspace?: boolean;
|
|
1981
2010
|
};
|
package/campaign.ts
CHANGED
|
@@ -249,7 +249,7 @@ function shift(args: string[], expected: string): string {
|
|
|
249
249
|
if (error instanceof CliArgError) throw new CampaignCliError(error.message);
|
|
250
250
|
throw error;
|
|
251
251
|
}
|
|
252
|
-
if (value
|
|
252
|
+
if (isFlagToken(value)) {
|
|
253
253
|
throw new CampaignCliError(
|
|
254
254
|
`Missing ${expected}: it is positional, so write it before or after the flags, e.g. dench campaign get "Founder outreach" --json`,
|
|
255
255
|
);
|
|
@@ -257,7 +257,17 @@ function shift(args: string[], expected: string): string {
|
|
|
257
257
|
return value;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
function isFlagToken(value: string): boolean {
|
|
261
|
+
return value.startsWith("--") || /^-[A-Za-z]/.test(value);
|
|
262
|
+
}
|
|
263
|
+
|
|
260
264
|
function unknownFlags(args: readonly string[], command: string): void {
|
|
265
|
+
const short = args.find((token) => /^-[A-Za-z]/.test(token));
|
|
266
|
+
if (short) {
|
|
267
|
+
throw new CampaignCliError(
|
|
268
|
+
`Unknown flag for ${command}: ${short}. Run \`dench campaign help\` for the full flag list.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
261
271
|
try {
|
|
262
272
|
assertNoUnknownFlags(args, command);
|
|
263
273
|
} catch (error) {
|
|
@@ -1298,11 +1308,17 @@ async function runGet(ctx: CampaignCliContext): Promise<void> {
|
|
|
1298
1308
|
unknownFlags(ctx.args, "campaign get");
|
|
1299
1309
|
const row = await resolveCampaign(ctx, reference);
|
|
1300
1310
|
const sequenceId = String(row._id);
|
|
1311
|
+
let analyticsError: string | undefined;
|
|
1301
1312
|
const [detail, analytics, live] = await Promise.all([
|
|
1302
1313
|
loadDetail(ctx, sequenceId),
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1314
|
+
(
|
|
1315
|
+
callAction(ctx, `${OUTREACH}:computeSequenceAnalytics`, {
|
|
1316
|
+
sequenceId,
|
|
1317
|
+
}) as Promise<CampaignAnalytics>
|
|
1318
|
+
).catch((error: unknown) => {
|
|
1319
|
+
analyticsError = error instanceof Error ? error.message : String(error);
|
|
1320
|
+
return null;
|
|
1321
|
+
}),
|
|
1306
1322
|
callQuery(ctx, `${OUTREACH}:getSequenceLive`, {
|
|
1307
1323
|
sequenceId,
|
|
1308
1324
|
}) as Promise<CampaignLive>,
|
|
@@ -1324,6 +1340,11 @@ async function runGet(ctx: CampaignCliContext): Promise<void> {
|
|
|
1324
1340
|
if (sequence.lastError) lines.push(`Last error: ${sequence.lastError}`);
|
|
1325
1341
|
|
|
1326
1342
|
lines.push("", "Funnel");
|
|
1343
|
+
if (analyticsError) {
|
|
1344
|
+
lines.push(
|
|
1345
|
+
` Funnel numbers are unavailable right now (${analyticsError}). Run dench campaign get again in a minute.`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1327
1348
|
const a = analytics ?? {};
|
|
1328
1349
|
const audience = a.audience ?? detail.enrollments.length;
|
|
1329
1350
|
lines.push(` Enrolled ${audience}`);
|
|
@@ -1418,7 +1439,11 @@ async function runGet(ctx: CampaignCliContext): Promise<void> {
|
|
|
1418
1439
|
` … ${enrollments.length - shown.length} more: dench campaign people <campaign>`,
|
|
1419
1440
|
);
|
|
1420
1441
|
}
|
|
1421
|
-
out(
|
|
1442
|
+
out(
|
|
1443
|
+
ctx,
|
|
1444
|
+
{ detail, analytics, live, ...(analyticsError ? { analyticsError } : {}) },
|
|
1445
|
+
lines,
|
|
1446
|
+
);
|
|
1422
1447
|
}
|
|
1423
1448
|
|
|
1424
1449
|
async function runSet(ctx: CampaignCliContext): Promise<void> {
|
|
@@ -1513,7 +1538,7 @@ async function runSteps(ctx: CampaignCliContext): Promise<void> {
|
|
|
1513
1538
|
}
|
|
1514
1539
|
if (row.status !== "draft") {
|
|
1515
1540
|
throw new CampaignCliError(
|
|
1516
|
-
"
|
|
1541
|
+
"steps set only works while a campaign is a draft. After launch, edit a step's text or wait time in the Dench app (the step types and order stay fixed).",
|
|
1517
1542
|
);
|
|
1518
1543
|
}
|
|
1519
1544
|
const raw = await readFile(file, "utf8").catch(() => {
|
|
@@ -1729,7 +1754,7 @@ async function runAdd(ctx: CampaignCliContext): Promise<void> {
|
|
|
1729
1754
|
unknownFlags(ctx.args, "campaign add");
|
|
1730
1755
|
|
|
1731
1756
|
const usage =
|
|
1732
|
-
'Usage: dench campaign add <campaign> --object <name> --view "<view>" | --object <name> --entries id1,id2 | --name "Ada"
|
|
1757
|
+
'Usage: dench campaign add <campaign> --object <name> --view "<view>" | --object <name> --entries id1,id2 | [--name "Ada"] --email ada@x.com and/or --linkedin-url https://linkedin.com/in/ada';
|
|
1733
1758
|
const row = await resolveCampaign(ctx, reference);
|
|
1734
1759
|
if (row.status === "archived") {
|
|
1735
1760
|
throw new CampaignCliError(
|
|
@@ -1738,6 +1763,11 @@ async function runAdd(ctx: CampaignCliContext): Promise<void> {
|
|
|
1738
1763
|
}
|
|
1739
1764
|
|
|
1740
1765
|
if (objectName) {
|
|
1766
|
+
if (displayName) {
|
|
1767
|
+
throw new CampaignCliError(
|
|
1768
|
+
`--name adds one person and can't be combined with --object. Use --name-field <column> to pick the name column. ${usage}`,
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1741
1771
|
if (viewName && entriesRaw) {
|
|
1742
1772
|
throw new CampaignCliError(
|
|
1743
1773
|
`Pass either --view or --entries, not both. ${usage}`,
|
|
@@ -1782,7 +1812,7 @@ async function runAdd(ctx: CampaignCliContext): Promise<void> {
|
|
|
1782
1812
|
if (collected.truncated) {
|
|
1783
1813
|
warn(
|
|
1784
1814
|
ctx,
|
|
1785
|
-
`the view "${viewName}"
|
|
1815
|
+
`the view "${viewName}" could not be read in full; ${entryIds.length} of ${collected.totalCount} rows were considered. Split it into smaller views to add the rest.`,
|
|
1786
1816
|
);
|
|
1787
1817
|
}
|
|
1788
1818
|
} else {
|
|
@@ -1979,14 +2009,32 @@ async function runLifecycle(
|
|
|
1979
2009
|
]);
|
|
1980
2010
|
}
|
|
1981
2011
|
|
|
2012
|
+
const ENROLLMENT_STATUSES = [
|
|
2013
|
+
"draft",
|
|
2014
|
+
"active",
|
|
2015
|
+
"paused",
|
|
2016
|
+
"finished",
|
|
2017
|
+
"replied",
|
|
2018
|
+
"stopped",
|
|
2019
|
+
"bounced",
|
|
2020
|
+
"complained",
|
|
2021
|
+
"unsubscribed",
|
|
2022
|
+
"failed",
|
|
2023
|
+
];
|
|
2024
|
+
|
|
1982
2025
|
async function runPeople(ctx: CampaignCliContext): Promise<void> {
|
|
1983
|
-
const status = valueFlag(ctx.args, "--status");
|
|
2026
|
+
const status = valueFlag(ctx.args, "--status")?.trim().toLowerCase();
|
|
1984
2027
|
const reference = shift(ctx.args, "campaign id or name");
|
|
1985
2028
|
unknownFlags(ctx.args, "campaign people");
|
|
2029
|
+
if (status !== undefined && !ENROLLMENT_STATUSES.includes(status)) {
|
|
2030
|
+
throw new CampaignCliError(
|
|
2031
|
+
`Unknown --status "${status}". Use one of: ${ENROLLMENT_STATUSES.join(", ")}.`,
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
1986
2034
|
const row = await resolveCampaign(ctx, reference);
|
|
1987
2035
|
const detail = await loadDetail(ctx, String(row._id));
|
|
1988
2036
|
const enrollments = status
|
|
1989
|
-
? detail.enrollments.filter((e) => e.status === status
|
|
2037
|
+
? detail.enrollments.filter((e) => e.status === status)
|
|
1990
2038
|
: detail.enrollments;
|
|
1991
2039
|
const people = enrollments.map((enrollment) =>
|
|
1992
2040
|
describePerson(enrollment, detail),
|
|
@@ -2307,8 +2355,45 @@ async function runAccounts(ctx: CampaignCliContext): Promise<void> {
|
|
|
2307
2355
|
|
|
2308
2356
|
// ─── Help ─────────────────────────────────────────────────────────────────
|
|
2309
2357
|
|
|
2310
|
-
|
|
2311
|
-
|
|
2358
|
+
const SUBCOMMAND_ALIASES: Record<string, string> = {
|
|
2359
|
+
ls: "list",
|
|
2360
|
+
new: "create",
|
|
2361
|
+
status: "get",
|
|
2362
|
+
show: "get",
|
|
2363
|
+
update: "set",
|
|
2364
|
+
configure: "set",
|
|
2365
|
+
enroll: "add",
|
|
2366
|
+
start: "launch",
|
|
2367
|
+
activity: "events",
|
|
2368
|
+
senders: "accounts",
|
|
2369
|
+
};
|
|
2370
|
+
|
|
2371
|
+
export function campaignHelp(subcommand?: string): void {
|
|
2372
|
+
const text = campaignHelpText();
|
|
2373
|
+
const name = subcommand
|
|
2374
|
+
? (SUBCOMMAND_ALIASES[subcommand] ?? subcommand)
|
|
2375
|
+
: undefined;
|
|
2376
|
+
const lines = text.split("\n");
|
|
2377
|
+
const usage: string[] = [];
|
|
2378
|
+
lines.forEach((line, index) => {
|
|
2379
|
+
if (!name || !line.startsWith(` dench campaign ${name} `)) return;
|
|
2380
|
+
usage.push(line);
|
|
2381
|
+
for (const next of lines.slice(index + 1)) {
|
|
2382
|
+
if (!next.startsWith(" ")) break;
|
|
2383
|
+
usage.push(next);
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
if (!usage.length) {
|
|
2387
|
+
console.log(text);
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
console.log(
|
|
2391
|
+
`Usage:\n${usage.join("\n")}\n\n<campaign> is an id or an exact (case-insensitive) name. Run 'dench campaign help' for everything.`,
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function campaignHelpText(): string {
|
|
2396
|
+
return `Usage: dench campaign <subcommand> [--json]
|
|
2312
2397
|
|
|
2313
2398
|
Outreach campaigns send email and LinkedIn steps to a list of people on one
|
|
2314
2399
|
schedule. <campaign> is an id or an exact (case-insensitive) name.
|
|
@@ -2316,7 +2401,7 @@ schedule. <campaign> is an id or an exact (case-insensitive) name.
|
|
|
2316
2401
|
Browse:
|
|
2317
2402
|
dench campaign list [--all] # live campaigns; --all includes archived
|
|
2318
2403
|
dench campaign get <campaign> # status, schedule, funnel, Now/Next, holds, people
|
|
2319
|
-
dench campaign people <campaign> [--status active|paused|finished|replied|stopped|bounced|failed]
|
|
2404
|
+
dench campaign people <campaign> [--status active|paused|finished|replied|stopped|bounced|failed|draft|complained|unsubscribed]
|
|
2320
2405
|
dench campaign events <campaign> [--limit 50] [--max-pages 1] # activity log, newest first
|
|
2321
2406
|
dench campaign accounts # email + LinkedIn sender ids, availability and capacity
|
|
2322
2407
|
|
|
@@ -2332,9 +2417,10 @@ Create and configure:
|
|
|
2332
2417
|
dench campaign steps set <campaign> --file steps.json # replaces every step; drafts only
|
|
2333
2418
|
|
|
2334
2419
|
Fill it:
|
|
2335
|
-
dench campaign add <campaign> --object lead --view "Q3 targets" [--email-field Email] [--linkedin-field "POC LinkedIn URL"]
|
|
2420
|
+
dench campaign add <campaign> --object lead --view "Q3 targets" [--email-field Email] [--linkedin-field "POC LinkedIn URL"] \\
|
|
2421
|
+
[--name-field Name] [--allow-duplicates]
|
|
2336
2422
|
dench campaign add <campaign> --object people --entries id1,id2 [--relation-field Contact] [--reverse]
|
|
2337
|
-
dench campaign add <campaign> --name "Ada Lovelace"
|
|
2423
|
+
dench campaign add <campaign> [--name "Ada Lovelace"] --email ada@x.com and/or --linkedin-url https://linkedin.com/in/ada
|
|
2338
2424
|
|
|
2339
2425
|
Run it:
|
|
2340
2426
|
dench campaign launch <campaign> [--ack-linkedin-risk] # checks readiness first; prints blockers
|
|
@@ -2360,13 +2446,14 @@ leave that channel unchanged. Only your accounts and accounts shared with you
|
|
|
2360
2446
|
can be selected. Launch checks availability. A person's sender stays fixed for
|
|
2361
2447
|
their conversation. Legacy step senders are preserved until a pool is configured.
|
|
2362
2448
|
|
|
2363
|
-
A campaign holds at most ${MAX_CAMPAIGN_PEOPLE} people.
|
|
2364
|
-
|
|
2449
|
+
A campaign holds at most ${MAX_CAMPAIGN_PEOPLE} people. steps set only works on drafts. After
|
|
2450
|
+
launch, edit a step's text or wait time in the Dench app; the schedule,
|
|
2451
|
+
daily cap and stop-on-reply stay editable here. Leads move New → Contacted → Replied
|
|
2365
2452
|
on their own as steps go out.
|
|
2366
2453
|
|
|
2367
2454
|
Auth: a workspace member's credentials (dench signin, a member DENCH_API_KEY, or
|
|
2368
2455
|
an agent session). An organization-only key cannot run outreach.
|
|
2369
|
-
|
|
2456
|
+
`;
|
|
2370
2457
|
}
|
|
2371
2458
|
|
|
2372
2459
|
// ─── Entry point ──────────────────────────────────────────────────────────
|
|
@@ -2395,6 +2482,10 @@ export async function runCampaignCommand(opts: {
|
|
|
2395
2482
|
campaignHelp();
|
|
2396
2483
|
return;
|
|
2397
2484
|
}
|
|
2485
|
+
if (args.some((token) => token === "--help" || token === "-h")) {
|
|
2486
|
+
campaignHelp(subcommand);
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2398
2489
|
switch (subcommand) {
|
|
2399
2490
|
case "list":
|
|
2400
2491
|
case "ls":
|
package/dench.ts
CHANGED
|
@@ -373,7 +373,7 @@ function normalizeCliError(error: unknown): CliError {
|
|
|
373
373
|
],
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
|
-
if (message.includes("Sign in as a workspace member to
|
|
376
|
+
if (message.includes("Sign in as a workspace member to")) {
|
|
377
377
|
return new CliError(
|
|
378
378
|
"Outreach needs a workspace member's credentials — sending accounts belong to a person, not the workspace",
|
|
379
379
|
{
|
|
@@ -4905,6 +4905,11 @@ async function main() {
|
|
|
4905
4905
|
await runToolCommand({ args: filteredSubArgs, jsonOutput: json });
|
|
4906
4906
|
return;
|
|
4907
4907
|
}
|
|
4908
|
+
if (sub === "run") {
|
|
4909
|
+
const { parseJson } = await import("./lib/cli-args");
|
|
4910
|
+
const argsIndex = filteredSubArgs.indexOf("--args");
|
|
4911
|
+
if (argsIndex !== -1) parseJson(filteredSubArgs[argsIndex + 1]);
|
|
4912
|
+
}
|
|
4908
4913
|
const runtime = await getRuntime();
|
|
4909
4914
|
const isSlackConnect =
|
|
4910
4915
|
sub === "connect" &&
|
|
@@ -5105,7 +5110,11 @@ async function main() {
|
|
|
5105
5110
|
if (command === "campaign" || command === "campaigns") {
|
|
5106
5111
|
const subArgs = stripRuntimeFlags(args.slice(args.indexOf(command) + 1));
|
|
5107
5112
|
const sub = subArgs[0];
|
|
5108
|
-
if (
|
|
5113
|
+
if (
|
|
5114
|
+
!sub ||
|
|
5115
|
+
sub === "help" ||
|
|
5116
|
+
subArgs.some((token) => token === "--help" || token === "-h")
|
|
5117
|
+
) {
|
|
5109
5118
|
const { runCampaignCommand } = await import("./campaign");
|
|
5110
5119
|
// biome-ignore lint/suspicious/noExplicitAny: help-only stub, never queries
|
|
5111
5120
|
await runCampaignCommand({ convex: {} as any, args: subArgs });
|
package/lib/api-schemas.ts
CHANGED
|
@@ -2382,6 +2382,31 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
|
|
|
2382
2382
|
.optional()
|
|
2383
2383
|
.describe("Epoch ms; null clears the end."),
|
|
2384
2384
|
sendSchedules: z.array(campaignSendSchedule).min(1).max(8).optional(),
|
|
2385
|
+
timezone: z
|
|
2386
|
+
.string()
|
|
2387
|
+
.optional()
|
|
2388
|
+
.describe("Legacy single-window timezone. Prefer sendSchedules."),
|
|
2389
|
+
sendWindowStartHour: z
|
|
2390
|
+
.number()
|
|
2391
|
+
.int()
|
|
2392
|
+
.min(0)
|
|
2393
|
+
.max(23)
|
|
2394
|
+
.optional()
|
|
2395
|
+
.describe("Legacy single-window start hour. Prefer sendSchedules."),
|
|
2396
|
+
sendWindowEndHour: z
|
|
2397
|
+
.number()
|
|
2398
|
+
.int()
|
|
2399
|
+
.min(1)
|
|
2400
|
+
.max(24)
|
|
2401
|
+
.optional()
|
|
2402
|
+
.describe("Legacy single-window end hour. Prefer sendSchedules."),
|
|
2403
|
+
sendWeekdays: z
|
|
2404
|
+
.array(z.number().int().min(0).max(6))
|
|
2405
|
+
.min(1)
|
|
2406
|
+
.optional()
|
|
2407
|
+
.describe(
|
|
2408
|
+
"Legacy single-window weekdays, 0 = Sunday. Prefer sendSchedules.",
|
|
2409
|
+
),
|
|
2385
2410
|
dailySendCap: z
|
|
2386
2411
|
.number()
|
|
2387
2412
|
.int()
|
|
@@ -2430,6 +2455,13 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
|
|
|
2430
2455
|
.describe("Existing CRM people entry to link."),
|
|
2431
2456
|
sourceEntryId: z.string().optional(),
|
|
2432
2457
|
sourceObjectName: z.string().optional(),
|
|
2458
|
+
emailIdentityId: z.string().optional(),
|
|
2459
|
+
linkedinIdentityId: z.string().optional(),
|
|
2460
|
+
linkedinProviderUserId: z.string().optional(),
|
|
2461
|
+
newRecordsObject: z
|
|
2462
|
+
.enum(["lead", "people"])
|
|
2463
|
+
.optional()
|
|
2464
|
+
.describe("CRM object a new record is created in when none matches."),
|
|
2433
2465
|
personalization: z.record(z.string(), z.unknown()).optional(),
|
|
2434
2466
|
checkDuplicatesAcrossWorkspace: z.boolean().optional(),
|
|
2435
2467
|
}),
|
|
@@ -3591,6 +3623,27 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
|
|
|
3591
3623
|
"email.campaign.pause": z.object({ campaignId: z.string() }),
|
|
3592
3624
|
"email.campaign.resume": z.object({ campaignId: z.string() }),
|
|
3593
3625
|
"email.campaign.cancel": z.object({ campaignId: z.string() }),
|
|
3626
|
+
"email.campaign.list": z.object({
|
|
3627
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
3628
|
+
cursor: z.coerce.string().optional(),
|
|
3629
|
+
status: z.string().optional(),
|
|
3630
|
+
}),
|
|
3631
|
+
"email.campaign.recipients.list": z.object({
|
|
3632
|
+
campaignId: z.string(),
|
|
3633
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
3634
|
+
cursor: z.coerce.string().optional(),
|
|
3635
|
+
status: z.string().optional(),
|
|
3636
|
+
includePeople: z
|
|
3637
|
+
.boolean()
|
|
3638
|
+
.optional()
|
|
3639
|
+
.describe("Default true: attach each recipient's CRM record."),
|
|
3640
|
+
}),
|
|
3641
|
+
"email.sequence.enrollments.list": z.object({
|
|
3642
|
+
sequenceId: z.string(),
|
|
3643
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
3644
|
+
cursor: z.coerce.string().optional(),
|
|
3645
|
+
status: z.string().optional(),
|
|
3646
|
+
}),
|
|
3594
3647
|
|
|
3595
3648
|
// Routines
|
|
3596
3649
|
"cron.list": z.object({ enabledOnly: z.coerce.boolean().optional() }),
|
|
@@ -5174,6 +5227,12 @@ export const requestExamples: Record<string, unknown> = {
|
|
|
5174
5227
|
objectName: "lead",
|
|
5175
5228
|
entryIds: ["<entryId1>", "<entryId2>"],
|
|
5176
5229
|
},
|
|
5230
|
+
"campaigns.people.add": {
|
|
5231
|
+
sequenceId: "<sequenceId>",
|
|
5232
|
+
displayName: "Ada Lovelace",
|
|
5233
|
+
email: "ada@example.com",
|
|
5234
|
+
linkedinProfileUrl: "https://linkedin.com/in/ada",
|
|
5235
|
+
},
|
|
5177
5236
|
"campaigns.launch": {
|
|
5178
5237
|
sequenceId: "<sequenceId>",
|
|
5179
5238
|
fingerprint: "<fingerprint from the launch preview>",
|
package/lib/command-registry.ts
CHANGED
|
@@ -3254,7 +3254,13 @@ const rawApiOperations = [
|
|
|
3254
3254
|
path: "/email/campaigns",
|
|
3255
3255
|
requestSchema: anyObject,
|
|
3256
3256
|
responseSchema: anyResponse,
|
|
3257
|
-
backend:
|
|
3257
|
+
backend: {
|
|
3258
|
+
...convex("query", "functions/emailCampaigns:listCampaigns"),
|
|
3259
|
+
transformArgs: (input) => ({
|
|
3260
|
+
status: input.status,
|
|
3261
|
+
paginationOpts: paginationOptsFrom(input, 25),
|
|
3262
|
+
}),
|
|
3263
|
+
},
|
|
3258
3264
|
}),
|
|
3259
3265
|
op({
|
|
3260
3266
|
id: "email.campaign.recipients.list",
|
|
@@ -3266,7 +3272,15 @@ const rawApiOperations = [
|
|
|
3266
3272
|
path: "/email/campaigns/{campaignId}/recipients",
|
|
3267
3273
|
requestSchema: anyObject,
|
|
3268
3274
|
responseSchema: anyResponse,
|
|
3269
|
-
backend:
|
|
3275
|
+
backend: {
|
|
3276
|
+
...convex("query", "functions/emailCampaigns:listRecipients"),
|
|
3277
|
+
transformArgs: (input) => ({
|
|
3278
|
+
campaignId: input.campaignId,
|
|
3279
|
+
status: input.status,
|
|
3280
|
+
includePeople: input.includePeople,
|
|
3281
|
+
paginationOpts: paginationOptsFrom(input, 50),
|
|
3282
|
+
}),
|
|
3283
|
+
},
|
|
3270
3284
|
}),
|
|
3271
3285
|
op({
|
|
3272
3286
|
id: "email.campaign.recipients.addFromObject",
|
|
@@ -3446,7 +3460,14 @@ const rawApiOperations = [
|
|
|
3446
3460
|
path: "/email/sequences/{sequenceId}/enrollments",
|
|
3447
3461
|
requestSchema: anyObject,
|
|
3448
3462
|
responseSchema: anyResponse,
|
|
3449
|
-
backend:
|
|
3463
|
+
backend: {
|
|
3464
|
+
...convex("query", "functions/emailSequences:listEnrollments"),
|
|
3465
|
+
transformArgs: (input) => ({
|
|
3466
|
+
sequenceId: input.sequenceId,
|
|
3467
|
+
status: input.status,
|
|
3468
|
+
paginationOpts: paginationOptsFrom(input, 50),
|
|
3469
|
+
}),
|
|
3470
|
+
},
|
|
3450
3471
|
}),
|
|
3451
3472
|
op({
|
|
3452
3473
|
id: "email.sequence.enrollment.pause",
|