@series-a/cli 0.6.2 → 0.13.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.
- package/CHANGELOG.md +39 -0
- package/README.md +62 -0
- package/package.json +1 -1
- package/src/api.mjs +26 -1
- package/src/commands/auth.mjs +45 -7
- package/src/commands/insights.mjs +194 -2
- package/src/commands/resources.mjs +346 -16
- package/src/commands/strategies.mjs +242 -0
- package/src/commands/voice.mjs +202 -0
- package/src/index.mjs +11 -3
- package/src/ui.mjs +47 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { call } from "../api.mjs";
|
|
3
|
-
import { emit, fail, ctxFrom, withCommonOpts, table, truncate } from "../ui.mjs";
|
|
3
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
|
|
4
4
|
|
|
5
5
|
function parseCsv(text) {
|
|
6
6
|
// Tiny CSV parser: header row + comma-separated values, quoted fields supported.
|
|
@@ -174,8 +174,10 @@ export function registerLeads(program) {
|
|
|
174
174
|
|
|
175
175
|
withCommonOpts(leads.command("delete <id>"))
|
|
176
176
|
.description("Delete a lead")
|
|
177
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
177
178
|
.action(async function (id) {
|
|
178
179
|
try {
|
|
180
|
+
await confirmDestructive(this, `Delete lead ${id}. This cannot be undone.`);
|
|
179
181
|
const r = await call("leads", "delete", { id }, ctxFrom(this));
|
|
180
182
|
emit(this, r, () => process.stdout.write(`✓ Deleted ${id}\n`));
|
|
181
183
|
} catch (e) { fail(e); }
|
|
@@ -291,11 +293,56 @@ export function registerCampaigns(program) {
|
|
|
291
293
|
} catch (e) { fail(e); }
|
|
292
294
|
});
|
|
293
295
|
|
|
294
|
-
withCommonOpts(c.command("
|
|
295
|
-
.description("
|
|
296
|
+
withCommonOpts(c.command("remove-leads <campaignId>"))
|
|
297
|
+
.description("Un-enrol leads from a campaign")
|
|
298
|
+
.option("--lead-ids <ids>", "comma-separated lead ids")
|
|
299
|
+
.option("--all", "remove every enrolled lead")
|
|
300
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
301
|
+
.option("--dry-run", "show what would be removed")
|
|
302
|
+
.action(async function (campaignId, opts) {
|
|
303
|
+
try {
|
|
304
|
+
const ids = opts.leadIds ? opts.leadIds.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
305
|
+
if (!ids.length && !opts.all) return fail(new Error("--lead-ids or --all required"));
|
|
306
|
+
const params = { campaign_id: campaignId, lead_ids: ids, all: !!opts.all };
|
|
307
|
+
if (opts.dryRun) {
|
|
308
|
+
const d = await call("campaigns", "remove_leads", { ...params, dry_run: true }, ctxFrom(this));
|
|
309
|
+
return emit(this, d, (x) => process.stdout.write(`Would remove ${x.would_remove} lead(s) from ${campaignId}\n`));
|
|
310
|
+
}
|
|
311
|
+
await confirmDestructive(this, `Remove ${opts.all ? "ALL enrolled leads" : `${ids.length} lead(s)`} from campaign ${campaignId}.`);
|
|
312
|
+
const r = await call("campaigns", "remove_leads", params, ctxFrom(this));
|
|
313
|
+
emit(this, r, (d) => process.stdout.write(`✓ Removed ${d.removed} lead(s)\n`));
|
|
314
|
+
} catch (e) { fail(e); }
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
withCommonOpts(c.command("preflight <id>"))
|
|
318
|
+
.description("Check whether a campaign is safe to launch (read-only)")
|
|
296
319
|
.action(async function (id) {
|
|
297
320
|
try {
|
|
298
|
-
const r = await call("campaigns", "
|
|
321
|
+
const r = await call("campaigns", "preflight", { campaign_id: id }, ctxFrom(this));
|
|
322
|
+
emit(this, r, renderPreflight);
|
|
323
|
+
} catch (e) { fail(e); }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
withCommonOpts(c.command("launch <id>"))
|
|
327
|
+
.description("Activate a campaign — runs preflight first, then requires --confirm")
|
|
328
|
+
.option("--confirm", "actually launch (without it, this is a dry run)")
|
|
329
|
+
.option("--force", "launch even if preflight found blockers")
|
|
330
|
+
.action(async function (id, opts) {
|
|
331
|
+
try {
|
|
332
|
+
if (!opts.confirm) {
|
|
333
|
+
const d = await call("campaigns", "launch", { campaign_id: id, dry_run: true }, ctxFrom(this));
|
|
334
|
+
return emit(this, d, (x) => {
|
|
335
|
+
renderPreflight(x.preflight ?? x);
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
x.preflight?.ok
|
|
338
|
+
? `\nDry run only. Re-run with --confirm to launch.\n`
|
|
339
|
+
: `\nDry run only. Fix the blockers above, then re-run with --confirm.\n`,
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const r = await call(
|
|
344
|
+
"campaigns", "launch", { campaign_id: id, force: !!opts.force }, ctxFrom(this),
|
|
345
|
+
);
|
|
299
346
|
emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
|
|
300
347
|
} catch (e) { fail(e); }
|
|
301
348
|
});
|
|
@@ -311,14 +358,84 @@ export function registerCampaigns(program) {
|
|
|
311
358
|
|
|
312
359
|
withCommonOpts(c.command("resume <id>"))
|
|
313
360
|
.description("Resume a paused campaign")
|
|
314
|
-
.
|
|
361
|
+
.option("--confirm", "required — resuming restarts sending")
|
|
362
|
+
.option("--force", "resume even if preflight found blockers")
|
|
363
|
+
.action(async function (id, opts) {
|
|
315
364
|
try {
|
|
316
|
-
|
|
365
|
+
if (!opts.confirm) {
|
|
366
|
+
const d = await call("campaigns", "resume", { campaign_id: id, dry_run: true }, ctxFrom(this));
|
|
367
|
+
return emit(this, d, (x) => {
|
|
368
|
+
renderPreflight(x.preflight ?? x);
|
|
369
|
+
process.stdout.write(`\nDry run only. Re-run with --confirm to resume.\n`);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const r = await call("campaigns", "resume", { campaign_id: id, force: !!opts.force }, ctxFrom(this));
|
|
317
373
|
emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
|
|
318
374
|
} catch (e) { fail(e); }
|
|
319
375
|
});
|
|
376
|
+
|
|
377
|
+
withCommonOpts(c.command("archive <id>"))
|
|
378
|
+
.description("Archive a campaign (reversible, keeps history)")
|
|
379
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
380
|
+
.action(async function (id) {
|
|
381
|
+
try {
|
|
382
|
+
await confirmDestructive(this, `Archive campaign ${id}. It will stop sending.`);
|
|
383
|
+
const r = await call("campaigns", "archive", { campaign_id: id }, ctxFrom(this));
|
|
384
|
+
emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → archived\n`));
|
|
385
|
+
} catch (e) { fail(e); }
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
withCommonOpts(c.command("clone <id>"))
|
|
389
|
+
.description("Duplicate a campaign as a fresh draft (no leads, no history)")
|
|
390
|
+
.option("--name <name>", "name for the copy")
|
|
391
|
+
.action(async function (id, opts) {
|
|
392
|
+
try {
|
|
393
|
+
const r = await call("campaigns", "clone", { campaign_id: id, name: opts.name }, ctxFrom(this));
|
|
394
|
+
emit(this, r, (d) => process.stdout.write(`✓ Cloned to ${d.campaign?.id} — ${d.campaign?.name}\n`));
|
|
395
|
+
} catch (e) { fail(e); }
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
withCommonOpts(c.command("delete <id>"))
|
|
399
|
+
.description("Permanently delete a campaign and its enrolments")
|
|
400
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
401
|
+
.option("--dry-run", "show what would be deleted")
|
|
402
|
+
.option("--force", "delete even when the campaign has dispatch history")
|
|
403
|
+
.action(async function (id, opts) {
|
|
404
|
+
try {
|
|
405
|
+
if (opts.dryRun) {
|
|
406
|
+
const d = await call("campaigns", "delete", { campaign_id: id, dry_run: true }, ctxFrom(this));
|
|
407
|
+
return emit(this, d, (x) =>
|
|
408
|
+
process.stdout.write(
|
|
409
|
+
`Would delete campaign ${x.campaign?.name ?? id} (${x.dispatches} dispatched actions)\n`,
|
|
410
|
+
));
|
|
411
|
+
}
|
|
412
|
+
await confirmDestructive(
|
|
413
|
+
this,
|
|
414
|
+
`Permanently delete campaign ${id} and every lead enrolment on it. This cannot be undone.`,
|
|
415
|
+
{ expect: id.slice(0, 8) },
|
|
416
|
+
);
|
|
417
|
+
const r = await call(
|
|
418
|
+
"campaigns", "delete", { campaign_id: id, force: !!opts.force }, ctxFrom(this),
|
|
419
|
+
);
|
|
420
|
+
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.name ?? id}\n`));
|
|
421
|
+
} catch (e) { fail(e); }
|
|
422
|
+
});
|
|
320
423
|
}
|
|
321
424
|
|
|
425
|
+
function renderPreflight(p) {
|
|
426
|
+
if (!p) return;
|
|
427
|
+
process.stdout.write(
|
|
428
|
+
`${p.ok ? "✓ Ready to launch" : "✖ Not ready"} — ${p.name ?? p.campaign_id} (${p.status})\n`,
|
|
429
|
+
);
|
|
430
|
+
const c = p.checks ?? {};
|
|
431
|
+
process.stdout.write(
|
|
432
|
+
` leads: ${c.leads_enrolled} steps: ${c.sequence_steps} senders: ${c.sender_accounts} daily limit: ${c.daily_send_limit ?? "—"}\n`,
|
|
433
|
+
);
|
|
434
|
+
for (const b of p.blockers ?? []) process.stdout.write(` ✖ ${b}\n`);
|
|
435
|
+
for (const w of p.warnings ?? []) process.stdout.write(` ! ${w}\n`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
322
439
|
export function registerSequences(program) {
|
|
323
440
|
const s = program.command("sequences").description("Manage LinkedIn sequences");
|
|
324
441
|
|
|
@@ -501,8 +618,68 @@ export function registerSequences(program) {
|
|
|
501
618
|
});
|
|
502
619
|
} catch (e) { fail(e); }
|
|
503
620
|
});
|
|
621
|
+
|
|
622
|
+
withCommonOpts(s.command("update-step <stepId>"))
|
|
623
|
+
.description("Patch one step in place (message, delay, variant, channel, action)")
|
|
624
|
+
.option("--action <action>", "step action")
|
|
625
|
+
.option("--message <text>", "message_content")
|
|
626
|
+
.option("--delay <days>", "delay_days")
|
|
627
|
+
.option("--variant <v>", "variant")
|
|
628
|
+
.option("--channel <c>", "channel")
|
|
629
|
+
.action(async function (stepId, opts) {
|
|
630
|
+
try {
|
|
631
|
+
const params = { step_id: stepId };
|
|
632
|
+
if (opts.action !== undefined) params.action = opts.action;
|
|
633
|
+
if (opts.message !== undefined) params.message_content = opts.message;
|
|
634
|
+
if (opts.delay !== undefined) params.delay_days = Number(opts.delay);
|
|
635
|
+
if (opts.variant !== undefined) params.variant = opts.variant;
|
|
636
|
+
if (opts.channel !== undefined) params.channel = opts.channel;
|
|
637
|
+
const r = await call("sequences", "update_step", params, ctxFrom(this));
|
|
638
|
+
emit(this, r, (d) => process.stdout.write(`✓ Updated step ${d.step?.id ?? stepId} (${d.step?.action})\n`));
|
|
639
|
+
} catch (e) { fail(e); }
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
withCommonOpts(s.command("delete-step <stepId>"))
|
|
643
|
+
.description("Delete one step; its children are re-parented")
|
|
644
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
645
|
+
.option("--dry-run", "show what would change")
|
|
646
|
+
.action(async function (stepId, opts) {
|
|
647
|
+
try {
|
|
648
|
+
if (opts.dryRun) {
|
|
649
|
+
const d = await call("sequences", "delete_step", { step_id: stepId, dry_run: true }, ctxFrom(this));
|
|
650
|
+
return emit(this, d, (x) =>
|
|
651
|
+
process.stdout.write(`Would delete step ${stepId}; ${x.reparented_children} child step(s) re-parented\n`));
|
|
652
|
+
}
|
|
653
|
+
await confirmDestructive(this, `Delete sequence step ${stepId}. Child steps will be re-parented.`);
|
|
654
|
+
const r = await call("sequences", "delete_step", { step_id: stepId }, ctxFrom(this));
|
|
655
|
+
emit(this, r, (d) =>
|
|
656
|
+
process.stdout.write(`✓ Deleted step ${stepId} (${d.reparented_children} child step(s) re-parented)\n`));
|
|
657
|
+
} catch (e) { fail(e); }
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
withCommonOpts(s.command("delete <id>"))
|
|
661
|
+
.description("Delete a sequence and all its steps")
|
|
662
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
663
|
+
.option("--dry-run", "show what would be deleted")
|
|
664
|
+
.option("--force", "delete even if campaigns still use it (they are detached)")
|
|
665
|
+
.action(async function (id, opts) {
|
|
666
|
+
try {
|
|
667
|
+
if (opts.dryRun) {
|
|
668
|
+
const d = await call("sequences", "delete", { id, dry_run: true }, ctxFrom(this));
|
|
669
|
+
return emit(this, d, (x) =>
|
|
670
|
+
process.stdout.write(
|
|
671
|
+
`Would delete sequence ${x.sequence?.name ?? id}; used by ${x.used_by_campaigns?.length ?? 0} campaign(s)\n`,
|
|
672
|
+
));
|
|
673
|
+
}
|
|
674
|
+
await confirmDestructive(this, `Delete sequence ${id} and every step on it. This cannot be undone.`);
|
|
675
|
+
const r = await call("sequences", "delete", { id, force: !!opts.force }, ctxFrom(this));
|
|
676
|
+
emit(this, r, (d) =>
|
|
677
|
+
process.stdout.write(`✓ Deleted sequence ${d.sequence_id} (${d.detached_campaigns} campaign(s) detached)\n`));
|
|
678
|
+
} catch (e) { fail(e); }
|
|
679
|
+
});
|
|
504
680
|
}
|
|
505
681
|
|
|
682
|
+
|
|
506
683
|
function parseSnoozeDuration(s) {
|
|
507
684
|
// Accepts e.g. "30m", "2h", "1d", "45" (minutes). Returns minutes (int).
|
|
508
685
|
if (!s) return null;
|
|
@@ -872,11 +1049,27 @@ export function registerContent(program) {
|
|
|
872
1049
|
});
|
|
873
1050
|
|
|
874
1051
|
withCommonOpts(c.command("schedule <id> <isoTimestamp>"))
|
|
875
|
-
.description("Schedule a content item")
|
|
876
|
-
.
|
|
1052
|
+
.description("Schedule a content item (Taplio when connected, otherwise built-in cron)")
|
|
1053
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1054
|
+
.option("--publish-as <p>", "personal | company")
|
|
1055
|
+
.option("--company-page <id>", "company page id when publishing as company")
|
|
1056
|
+
.option("--timezone <tz>", "IANA timezone label to store")
|
|
1057
|
+
.action(async function (id, ts, opts) {
|
|
1058
|
+
try {
|
|
1059
|
+
const r = await call("content", "schedule", {
|
|
1060
|
+
id, scheduled_for: ts, channel: opts.channel,
|
|
1061
|
+
publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
|
|
1062
|
+
}, ctxFrom(this));
|
|
1063
|
+
emit(this, r, (d) => process.stdout.write(`✓ Scheduled via ${d.engine} for ${d.scheduled_for}\n`));
|
|
1064
|
+
} catch (e) { fail(e); }
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
withCommonOpts(c.command("unschedule <id>"))
|
|
1068
|
+
.description("Cancel a scheduled content item on whichever engine holds it")
|
|
1069
|
+
.action(async function (id) {
|
|
877
1070
|
try {
|
|
878
|
-
const r = await call("content", "
|
|
879
|
-
emit(this, r, (d) => process.stdout.write(`✓
|
|
1071
|
+
const r = await call("content", "unschedule", { id }, ctxFrom(this));
|
|
1072
|
+
emit(this, r, (d) => process.stdout.write(`✓ Unscheduled ${d.unscheduled ?? id}${d.engine ? ` (${d.engine})` : ""}\n`));
|
|
880
1073
|
} catch (e) { fail(e); }
|
|
881
1074
|
});
|
|
882
1075
|
|
|
@@ -889,19 +1082,64 @@ export function registerContent(program) {
|
|
|
889
1082
|
} catch (e) { fail(e); }
|
|
890
1083
|
});
|
|
891
1084
|
|
|
1085
|
+
withCommonOpts(c.command("bulk-approve [ids...]"))
|
|
1086
|
+
.description("Approve many content items (all pending when no ids given)")
|
|
1087
|
+
.option("--campaign <id>", "only pending items in this campaign")
|
|
1088
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1089
|
+
.action(async function (ids, opts) {
|
|
1090
|
+
try {
|
|
1091
|
+
if (!ids?.length) await confirmDestructive(this, "Approve every pending content item in this workspace.");
|
|
1092
|
+
const r = await call("content", "bulk_approve", {
|
|
1093
|
+
ids: ids?.length ? ids : undefined, campaign_id: opts.campaign,
|
|
1094
|
+
}, ctxFrom(this));
|
|
1095
|
+
emit(this, r, (d) => process.stdout.write(`✓ Approved ${d.approved} items\n`));
|
|
1096
|
+
} catch (e) { fail(e); }
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
withCommonOpts(c.command("bulk-schedule <ids...>"))
|
|
1100
|
+
.description("Schedule several content items on a cadence via the live engine")
|
|
1101
|
+
.option("--start <iso>", "first slot (default: in 1 hour)")
|
|
1102
|
+
.option("--interval-hours <n>", "hours between posts", "24")
|
|
1103
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1104
|
+
.option("--publish-as <p>", "personal | company")
|
|
1105
|
+
.action(async function (ids, opts) {
|
|
1106
|
+
try {
|
|
1107
|
+
const r = await call("content", "bulk_schedule", {
|
|
1108
|
+
ids, start: opts.start, interval_hours: Number(opts.intervalHours),
|
|
1109
|
+
channel: opts.channel, publish_as: opts.publishAs,
|
|
1110
|
+
}, ctxFrom(this));
|
|
1111
|
+
emit(this, r, (d) => {
|
|
1112
|
+
process.stdout.write(table(
|
|
1113
|
+
["id", "ok", "engine", "when / error"],
|
|
1114
|
+
(d.results || []).map((x) => [truncate(x.id, 8), x.ok ? "yes" : "no", x.engine ?? "—", x.scheduled_for ?? truncate(x.error ?? "", 50)]),
|
|
1115
|
+
) + `\n${d.scheduled} scheduled, ${d.failed} failed\n`);
|
|
1116
|
+
});
|
|
1117
|
+
} catch (e) { fail(e); }
|
|
1118
|
+
});
|
|
1119
|
+
|
|
892
1120
|
withCommonOpts(c.command("publish <id>"))
|
|
893
|
-
.description("
|
|
894
|
-
.
|
|
1121
|
+
.description("Publish a content item immediately")
|
|
1122
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1123
|
+
.option("--publish-as <p>", "personal | company")
|
|
1124
|
+
.option("--company-page <id>", "company page id when publishing as company")
|
|
1125
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1126
|
+
.action(async function (id, opts) {
|
|
895
1127
|
try {
|
|
896
|
-
|
|
897
|
-
|
|
1128
|
+
await confirmDestructive(this, `Publish content item ${id} right now.`);
|
|
1129
|
+
const r = await call("content", "publish_now", {
|
|
1130
|
+
id, channel: opts.channel, publish_as: opts.publishAs, company_page_id: opts.companyPage,
|
|
1131
|
+
}, ctxFrom(this));
|
|
1132
|
+
emit(this, r, (d) => process.stdout.write(`✓ Published via ${d.worker}\n`));
|
|
898
1133
|
} catch (e) { fail(e); }
|
|
899
1134
|
});
|
|
900
1135
|
|
|
1136
|
+
|
|
901
1137
|
withCommonOpts(c.command("delete <id>"))
|
|
902
1138
|
.description("Delete a content item")
|
|
1139
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
903
1140
|
.action(async function (id) {
|
|
904
1141
|
try {
|
|
1142
|
+
await confirmDestructive(this, `Delete content item ${id}. This cannot be undone.`);
|
|
905
1143
|
const r = await call("content", "delete", { id }, ctxFrom(this));
|
|
906
1144
|
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
907
1145
|
} catch (e) { fail(e); }
|
|
@@ -1002,11 +1240,13 @@ export function registerCalendar(program) {
|
|
|
1002
1240
|
.option("--cta <text>", "call to action")
|
|
1003
1241
|
.option("--campaign <id>", "campaign_id")
|
|
1004
1242
|
.option("--status <s>", "draft | scheduled | published", "draft")
|
|
1243
|
+
.option("--at <iso>", "ISO timestamp — schedules the post immediately via the live engine")
|
|
1005
1244
|
.action(async function (opts) {
|
|
1006
1245
|
try {
|
|
1007
1246
|
let content = opts.content;
|
|
1008
1247
|
if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
|
|
1009
1248
|
const r = await call("calendar", "create", {
|
|
1249
|
+
scheduled_for: opts.at,
|
|
1010
1250
|
title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
|
|
1011
1251
|
platform: opts.platform, content: content ?? "",
|
|
1012
1252
|
hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
|
|
@@ -1042,15 +1282,99 @@ export function registerCalendar(program) {
|
|
|
1042
1282
|
});
|
|
1043
1283
|
|
|
1044
1284
|
withCommonOpts(cal.command("delete <id>"))
|
|
1045
|
-
.description("Delete a calendar post")
|
|
1285
|
+
.description("Delete a calendar post (cancels it on Taplio first when applicable)")
|
|
1286
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1046
1287
|
.action(async function (id) {
|
|
1047
1288
|
try {
|
|
1289
|
+
await confirmDestructive(this, `Delete calendar post ${id}. This cannot be undone.`);
|
|
1048
1290
|
const r = await call("calendar", "delete", { id }, ctxFrom(this));
|
|
1049
|
-
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
1291
|
+
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted ?? d.unscheduled ?? id}\n`));
|
|
1292
|
+
} catch (e) { fail(e); }
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
withCommonOpts(cal.command("engine"))
|
|
1296
|
+
.description("Show which scheduler is live for this workspace (Taplio or built-in cron)")
|
|
1297
|
+
.option("--channel <c>", "linkedin | blog | newsletter", "linkedin")
|
|
1298
|
+
.action(async function (opts) {
|
|
1299
|
+
try {
|
|
1300
|
+
const r = await call("calendar", "engine", { channel: opts.channel }, ctxFrom(this));
|
|
1301
|
+
emit(this, r, (d) => {
|
|
1302
|
+
process.stdout.write(
|
|
1303
|
+
`Engine: ${d.engine}\n` +
|
|
1304
|
+
`Channel: ${d.channel}\n` +
|
|
1305
|
+
`Taplio connected: ${d.taplio_connected ? "yes" : "no"}\n` +
|
|
1306
|
+
`Dispatch worker: ${d.dispatch_worker}\n` +
|
|
1307
|
+
`Reason: ${d.reason}\n` +
|
|
1308
|
+
`Pending scheduled: ${d.scheduled_pending}\n` +
|
|
1309
|
+
(d.recent_failures?.length
|
|
1310
|
+
? `\nRecent failures:\n` + table(
|
|
1311
|
+
["id", "when", "attempts", "error"],
|
|
1312
|
+
d.recent_failures.map((f) => [truncate(f.id, 8), f.scheduled_for ?? "—", f.dispatch_attempts ?? 0, truncate(f.dispatch_error ?? "", 60)]),
|
|
1313
|
+
) + "\n"
|
|
1314
|
+
: ""),
|
|
1315
|
+
);
|
|
1316
|
+
});
|
|
1317
|
+
} catch (e) { fail(e); }
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
withCommonOpts(cal.command("schedule <id>"))
|
|
1321
|
+
.description("Schedule (or reschedule) a calendar post via the live engine")
|
|
1322
|
+
.requiredOption("--at <iso>", "ISO timestamp, e.g. 2026-09-10T09:00:00Z")
|
|
1323
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1324
|
+
.option("--publish-as <p>", "personal | company")
|
|
1325
|
+
.option("--company-page <id>", "company page id when publishing as company")
|
|
1326
|
+
.option("--timezone <tz>", "IANA timezone label to store")
|
|
1327
|
+
.action(async function (id, opts) {
|
|
1328
|
+
try {
|
|
1329
|
+
const r = await call("calendar", "schedule", {
|
|
1330
|
+
id, scheduled_for: opts.at, channel: opts.channel,
|
|
1331
|
+
publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
|
|
1332
|
+
}, ctxFrom(this));
|
|
1333
|
+
emit(this, r, (d) => process.stdout.write(`✓ Scheduled via ${d.engine} for ${d.scheduled_for}\n`));
|
|
1334
|
+
} catch (e) { fail(e); }
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
withCommonOpts(cal.command("unschedule <id>"))
|
|
1338
|
+
.description("Cancel a scheduled calendar post (cancels on Taplio when it owns it)")
|
|
1339
|
+
.action(async function (id) {
|
|
1340
|
+
try {
|
|
1341
|
+
const r = await call("calendar", "unschedule", { id }, ctxFrom(this));
|
|
1342
|
+
emit(this, r, (d) => process.stdout.write(`✓ Unscheduled ${d.unscheduled} (${d.engine})\n`));
|
|
1343
|
+
} catch (e) { fail(e); }
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
withCommonOpts(cal.command("publish-now <id>"))
|
|
1347
|
+
.description("Publish a calendar post immediately")
|
|
1348
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1349
|
+
.action(async function (id) {
|
|
1350
|
+
try {
|
|
1351
|
+
await confirmDestructive(this, `Publish calendar post ${id} right now.`);
|
|
1352
|
+
const r = await call("calendar", "publish_now", { id }, ctxFrom(this));
|
|
1353
|
+
emit(this, r, (d) => process.stdout.write(`✓ Published via ${d.worker}\n`));
|
|
1354
|
+
} catch (e) { fail(e); }
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
withCommonOpts(cal.command("bulk-schedule <ids...>"))
|
|
1358
|
+
.description("Schedule several calendar posts on a cadence")
|
|
1359
|
+
.option("--start <iso>", "first slot (default: in 1 hour)")
|
|
1360
|
+
.option("--interval-hours <n>", "hours between posts", "24")
|
|
1361
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1362
|
+
.action(async function (ids, opts) {
|
|
1363
|
+
try {
|
|
1364
|
+
const r = await call("calendar", "bulk_schedule", {
|
|
1365
|
+
ids, start: opts.start, interval_hours: Number(opts.intervalHours), channel: opts.channel,
|
|
1366
|
+
}, ctxFrom(this));
|
|
1367
|
+
emit(this, r, (d) => {
|
|
1368
|
+
process.stdout.write(table(
|
|
1369
|
+
["id", "ok", "engine", "when / error"],
|
|
1370
|
+
(d.results || []).map((x) => [truncate(x.id, 8), x.ok ? "yes" : "no", x.engine ?? "—", x.scheduled_for ?? truncate(x.error ?? "", 50)]),
|
|
1371
|
+
) + `\n${d.scheduled} scheduled, ${d.failed} failed\n`);
|
|
1372
|
+
});
|
|
1050
1373
|
} catch (e) { fail(e); }
|
|
1051
1374
|
});
|
|
1052
1375
|
}
|
|
1053
1376
|
|
|
1377
|
+
|
|
1054
1378
|
export function registerSignals(program) {
|
|
1055
1379
|
const s = program.command("signals").description("Read and manage market intelligence signals");
|
|
1056
1380
|
|
|
@@ -1152,8 +1476,10 @@ export function registerSignals(program) {
|
|
|
1152
1476
|
|
|
1153
1477
|
withCommonOpts(s.command("delete <id>"))
|
|
1154
1478
|
.description("Delete a signal")
|
|
1479
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1155
1480
|
.action(async function (id) {
|
|
1156
1481
|
try {
|
|
1482
|
+
await confirmDestructive(this, `Delete signal ${id}. This cannot be undone.`);
|
|
1157
1483
|
const r = await call("signals", "delete", { id }, ctxFrom(this));
|
|
1158
1484
|
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
1159
1485
|
} catch (e) { fail(e); }
|
|
@@ -1283,8 +1609,10 @@ export function registerPipelines(program) {
|
|
|
1283
1609
|
|
|
1284
1610
|
withCommonOpts(p.command("delete <id>"))
|
|
1285
1611
|
.description("Delete an opportunity")
|
|
1612
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1286
1613
|
.action(async function (id) {
|
|
1287
1614
|
try {
|
|
1615
|
+
await confirmDestructive(this, `Delete pipeline record ${id}. This cannot be undone.`);
|
|
1288
1616
|
const r = await call("pipelines", "delete", { id }, ctxFrom(this));
|
|
1289
1617
|
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
1290
1618
|
} catch (e) { fail(e); }
|
|
@@ -1472,8 +1800,10 @@ export function registerAgents(program) {
|
|
|
1472
1800
|
|
|
1473
1801
|
withCommonOpts(a.command("delete <id>"))
|
|
1474
1802
|
.description("Delete an agent")
|
|
1803
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1475
1804
|
.action(async function (id) {
|
|
1476
1805
|
try {
|
|
1806
|
+
await confirmDestructive(this, `Delete agent ${id}. This cannot be undone.`);
|
|
1477
1807
|
const r = await call("agents", "delete", { id }, ctxFrom(this));
|
|
1478
1808
|
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
1479
1809
|
} catch (e) { fail(e); }
|