@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.
@@ -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("launch <id>"))
295
- .description("Activate a campaign (status → active)")
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", "launch", { campaign_id: id }, ctxFrom(this));
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
- .action(async function (id) {
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
- const r = await call("campaigns", "resume", { campaign_id: id }, ctxFrom(this));
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
- .action(async function (id, ts) {
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", "schedule", { id, scheduled_date: ts }, ctxFrom(this));
879
- emit(this, r, (d) => process.stdout.write(`✓ Scheduled ${d.content?.id} → ${d.content?.scheduled_date}\n`));
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("Dispatch publication via agent-content-posting")
894
- .action(async function (id) {
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
- const r = await call("content", "publish", { id }, ctxFrom(this));
897
- emit(this, r, (d) => process.stdout.write(JSON.stringify(d.publish, null, 2) + "\n"));
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); }