@series-a/cli 0.7.1 → 0.15.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 +55 -0
- package/package.json +1 -1
- package/src/api.mjs +21 -0
- package/src/commands/auth.mjs +3 -2
- package/src/commands/insights.mjs +545 -2
- package/src/commands/resources.mjs +153 -10
- package/src/commands/strategies.mjs +242 -0
- package/src/commands/voice.mjs +202 -0
- package/src/index.mjs +10 -2
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
import { readFile, writeFile } 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
|
const n = (v) => (v == null ? "" : String(v));
|
|
6
6
|
|
|
7
|
+
/** Minimal CSV reader: header row + quoted fields. */
|
|
8
|
+
function parseCsvLite(text) {
|
|
9
|
+
const lines = String(text).split(/\r?\n/).filter((l) => l.trim().length);
|
|
10
|
+
if (!lines.length) return [];
|
|
11
|
+
const split = (line) => {
|
|
12
|
+
const out = []; let cur = ""; let q = false;
|
|
13
|
+
for (let i = 0; i < line.length; i++) {
|
|
14
|
+
const c = line[i];
|
|
15
|
+
if (q) {
|
|
16
|
+
if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
|
|
17
|
+
else if (c === '"') q = false;
|
|
18
|
+
else cur += c;
|
|
19
|
+
} else if (c === ",") { out.push(cur); cur = ""; }
|
|
20
|
+
else if (c === '"') q = true;
|
|
21
|
+
else cur += c;
|
|
22
|
+
}
|
|
23
|
+
out.push(cur);
|
|
24
|
+
return out.map((x) => x.trim());
|
|
25
|
+
};
|
|
26
|
+
const headers = split(lines[0]).map((h) => h.toLowerCase());
|
|
27
|
+
return lines.slice(1).map((line) => {
|
|
28
|
+
const cells = split(line);
|
|
29
|
+
const row = {};
|
|
30
|
+
headers.forEach((h, i) => { row[h] = cells[i] ?? ""; });
|
|
31
|
+
return row;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
7
36
|
function findGroup(program, name) {
|
|
8
37
|
return program.commands.find((c) => c.name() === name);
|
|
9
38
|
}
|
|
@@ -608,6 +637,65 @@ export function registerAudiences(program) {
|
|
|
608
637
|
} catch (e) { fail(e); }
|
|
609
638
|
});
|
|
610
639
|
|
|
640
|
+
withCommonOpts(a.command("create"))
|
|
641
|
+
.description("Create a smart audience (lead segment)")
|
|
642
|
+
.requiredOption("--name <name>", "audience name")
|
|
643
|
+
.option("--description <text>", "description")
|
|
644
|
+
.option("--type <t>", "segment type", "manual")
|
|
645
|
+
.action(async function () {
|
|
646
|
+
try {
|
|
647
|
+
const o = this.optsWithGlobals();
|
|
648
|
+
const r = await call("audiences", "create", {
|
|
649
|
+
name: o.name, description: o.description, segment_type: o.type,
|
|
650
|
+
}, ctxFrom(this));
|
|
651
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Created audience ${d.audience?.id}\n`));
|
|
652
|
+
} catch (e) { fail(e); }
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
withCommonOpts(a.command("edit <id>"))
|
|
656
|
+
.description("Rename or re-describe an audience")
|
|
657
|
+
.option("--name <name>").option("--description <text>").option("--type <t>")
|
|
658
|
+
.action(async function (id) {
|
|
659
|
+
try {
|
|
660
|
+
const o = this.optsWithGlobals();
|
|
661
|
+
const params = { id };
|
|
662
|
+
if (o.name !== undefined) params.name = o.name;
|
|
663
|
+
if (o.description !== undefined) params.description = o.description;
|
|
664
|
+
if (o.type !== undefined) params.segment_type = o.type;
|
|
665
|
+
const r = await call("audiences", "update", params, ctxFrom(this));
|
|
666
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Updated ${d.audience?.id ?? id}\n`));
|
|
667
|
+
} catch (e) { fail(e); }
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
withCommonOpts(a.command("delete <id>"))
|
|
671
|
+
.description("Delete an audience and its membership rows")
|
|
672
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
673
|
+
.action(async function (id) {
|
|
674
|
+
try {
|
|
675
|
+
await confirmDestructive(this, `Delete audience ${id} and its membership rows.`);
|
|
676
|
+
const r = await call("audiences", "delete", { id }, ctxFrom(this));
|
|
677
|
+
emit(this, r, () => process.stdout.write(`\u2714 Deleted ${id}\n`));
|
|
678
|
+
} catch (e) { fail(e); }
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
withCommonOpts(a.command("add-leads <id> <leadIds...>"))
|
|
682
|
+
.description("Add leads to an audience")
|
|
683
|
+
.action(async function (id, leadIds) {
|
|
684
|
+
try {
|
|
685
|
+
const r = await call("audiences", "add_members", { id, lead_ids: leadIds }, ctxFrom(this));
|
|
686
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Added ${d.added ?? 0} of ${d.candidates ?? leadIds.length}\n`));
|
|
687
|
+
} catch (e) { fail(e); }
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
withCommonOpts(a.command("remove-leads <id> <leadIds...>"))
|
|
691
|
+
.description("Remove leads from an audience")
|
|
692
|
+
.action(async function (id, leadIds) {
|
|
693
|
+
try {
|
|
694
|
+
const r = await call("audiences", "remove_members", { id, lead_ids: leadIds }, ctxFrom(this));
|
|
695
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Removed ${d.removed ?? 0}\n`));
|
|
696
|
+
} catch (e) { fail(e); }
|
|
697
|
+
});
|
|
698
|
+
|
|
611
699
|
withCommonOpts(a.command("add-to-campaign <id> <campaignId>"))
|
|
612
700
|
.description("Enrol every lead in an audience into a campaign")
|
|
613
701
|
.action(async function (id, campaignId) {
|
|
@@ -740,6 +828,208 @@ export function registerNewsletter(program) {
|
|
|
740
828
|
});
|
|
741
829
|
} catch (e) { fail(e); }
|
|
742
830
|
});
|
|
831
|
+
|
|
832
|
+
withCommonOpts(nl.command("audience-show <audienceId>"))
|
|
833
|
+
.description("Show one mailing audience with contact counts")
|
|
834
|
+
.action(async function (audienceId) {
|
|
835
|
+
try {
|
|
836
|
+
const r = await call("newsletter", "audience_get", { audience_id: audienceId }, ctxFrom(this));
|
|
837
|
+
emit(this, r, (d) => process.stdout.write(
|
|
838
|
+
`${d.audience?.name}\n${d.stats?.contacts ?? 0} contacts (${d.stats?.subscribed ?? 0} subscribed, ${d.stats?.unsubscribed ?? 0} unsubscribed)\n` +
|
|
839
|
+
`Resend mirror: ${d.audience?.resend_audience_id || "(none — local only)"}\n`,
|
|
840
|
+
));
|
|
841
|
+
} catch (e) { fail(e); }
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
withCommonOpts(nl.command("audience-create"))
|
|
845
|
+
.description("Create a mailing audience (mirrored to Resend when connected)")
|
|
846
|
+
.requiredOption("--name <name>", "audience name")
|
|
847
|
+
.option("--description <text>", "description")
|
|
848
|
+
.action(async function () {
|
|
849
|
+
try {
|
|
850
|
+
const o = this.optsWithGlobals();
|
|
851
|
+
const r = await call("newsletter", "audience_create", {
|
|
852
|
+
name: o.name, description: o.description,
|
|
853
|
+
}, ctxFrom(this));
|
|
854
|
+
emit(this, r, (d) => process.stdout.write(
|
|
855
|
+
`\u2714 Created audience ${d.audience?.id}${d.resend_mirrored ? " (mirrored to Resend)" : " (local only — Resend not connected)"}\n`,
|
|
856
|
+
));
|
|
857
|
+
} catch (e) { fail(e); }
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
withCommonOpts(nl.command("audience-edit <audienceId>"))
|
|
861
|
+
.description("Rename or re-describe a mailing audience")
|
|
862
|
+
.option("--name <name>").option("--description <text>")
|
|
863
|
+
.action(async function (audienceId) {
|
|
864
|
+
try {
|
|
865
|
+
const o = this.optsWithGlobals();
|
|
866
|
+
const params = { audience_id: audienceId };
|
|
867
|
+
if (o.name !== undefined) params.name = o.name;
|
|
868
|
+
if (o.description !== undefined) params.description = o.description;
|
|
869
|
+
const r = await call("newsletter", "audience_update", params, ctxFrom(this));
|
|
870
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Updated ${d.audience?.id ?? audienceId}\n`));
|
|
871
|
+
} catch (e) { fail(e); }
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
withCommonOpts(nl.command("audience-delete <audienceId>"))
|
|
875
|
+
.description("Delete a mailing audience and its contacts")
|
|
876
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
877
|
+
.action(async function (audienceId) {
|
|
878
|
+
try {
|
|
879
|
+
await confirmDestructive(this, `Delete mailing audience ${audienceId} and every contact in it.`);
|
|
880
|
+
const r = await call("newsletter", "audience_delete", { audience_id: audienceId }, ctxFrom(this));
|
|
881
|
+
emit(this, r, () => process.stdout.write(`\u2714 Deleted ${audienceId}\n`));
|
|
882
|
+
} catch (e) { fail(e); }
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
withCommonOpts(nl.command("contact-add <audienceId> <email>"))
|
|
886
|
+
.description("Add one contact to a mailing audience")
|
|
887
|
+
.option("--first <name>").option("--last <name>")
|
|
888
|
+
.action(async function (audienceId, email) {
|
|
889
|
+
try {
|
|
890
|
+
const o = this.optsWithGlobals();
|
|
891
|
+
const r = await call("newsletter", "contact_add", {
|
|
892
|
+
audience_id: audienceId, email, first_name: o.first, last_name: o.last,
|
|
893
|
+
}, ctxFrom(this));
|
|
894
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Added ${d.added} contact(s)\n`));
|
|
895
|
+
} catch (e) { fail(e); }
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
withCommonOpts(nl.command("contacts-import <audienceId>"))
|
|
899
|
+
.description("Bulk import contacts from a CSV file (email,first_name,last_name)")
|
|
900
|
+
.requiredOption("--file <path>", "CSV file path")
|
|
901
|
+
.action(async function (audienceId) {
|
|
902
|
+
try {
|
|
903
|
+
const o = this.optsWithGlobals();
|
|
904
|
+
const rows = parseCsvLite(await readFile(o.file, "utf8"));
|
|
905
|
+
const contacts = rows.map((r) => ({
|
|
906
|
+
email: r.email || r["email address"] || "",
|
|
907
|
+
first_name: r.first_name || r.first || undefined,
|
|
908
|
+
last_name: r.last_name || r.last || undefined,
|
|
909
|
+
})).filter((c) => c.email);
|
|
910
|
+
if (!contacts.length) throw new Error("No email column found in the CSV");
|
|
911
|
+
const r = await call("newsletter", "contacts_import", {
|
|
912
|
+
audience_id: audienceId, contacts, source: "cli:import",
|
|
913
|
+
}, ctxFrom(this));
|
|
914
|
+
emit(this, r, (d) => process.stdout.write(
|
|
915
|
+
`\u2714 Imported ${d.added} of ${d.candidates} (Resend mirrored: ${d.resend_mirrored})\n`,
|
|
916
|
+
));
|
|
917
|
+
} catch (e) { fail(e); }
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
withCommonOpts(nl.command("contact-remove <email>"))
|
|
921
|
+
.description("Delete a contact (optionally only from one audience)")
|
|
922
|
+
.option("--audience <id>", "restrict to one audience")
|
|
923
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
924
|
+
.action(async function (email) {
|
|
925
|
+
try {
|
|
926
|
+
const o = this.optsWithGlobals();
|
|
927
|
+
await confirmDestructive(this, `Delete contact ${email}.`);
|
|
928
|
+
const r = await call("newsletter", "contact_remove", {
|
|
929
|
+
email, audience_id: o.audience,
|
|
930
|
+
}, ctxFrom(this));
|
|
931
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Removed ${d.removed}\n`));
|
|
932
|
+
} catch (e) { fail(e); }
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
withCommonOpts(nl.command("contact-unsubscribe <email>"))
|
|
936
|
+
.description("Mark a contact unsubscribed")
|
|
937
|
+
.option("--audience <id>", "restrict to one audience")
|
|
938
|
+
.action(async function (email) {
|
|
939
|
+
try {
|
|
940
|
+
const o = this.optsWithGlobals();
|
|
941
|
+
const r = await call("newsletter", "contact_unsubscribe", {
|
|
942
|
+
email, audience_id: o.audience,
|
|
943
|
+
}, ctxFrom(this));
|
|
944
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 Unsubscribed ${d.unsubscribed}\n`));
|
|
945
|
+
} catch (e) { fail(e); }
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
withCommonOpts(nl.command("sync-segment <segmentId> <audienceId>"))
|
|
949
|
+
.description("Push every lead in a smart audience into a mailing audience")
|
|
950
|
+
.option("--include-unverified", "include leads without a verified email")
|
|
951
|
+
.action(async function (segmentId, audienceId) {
|
|
952
|
+
try {
|
|
953
|
+
const o = this.optsWithGlobals();
|
|
954
|
+
const r = await call("newsletter", "sync_segment", {
|
|
955
|
+
segment_id: segmentId, audience_id: audienceId, include_unverified: !!o.includeUnverified,
|
|
956
|
+
}, ctxFrom(this));
|
|
957
|
+
emit(this, r, (d) => process.stdout.write(
|
|
958
|
+
`\u2714 Synced: ${d.added ?? 0} added, ${d.skipped ?? 0} skipped, ${d.failed ?? 0} failed\n`,
|
|
959
|
+
));
|
|
960
|
+
} catch (e) { fail(e); }
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
withCommonOpts(nl.command("send <id>"))
|
|
964
|
+
.description("Send a newsletter now, or schedule it, to one or more mailing audiences")
|
|
965
|
+
.option("--audience <ids>", "comma-separated mailing audience ids")
|
|
966
|
+
.option("--at <isoTimestamp>", "schedule instead of sending now")
|
|
967
|
+
.option("--from <email>", "override the from address")
|
|
968
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
969
|
+
.action(async function (id) {
|
|
970
|
+
try {
|
|
971
|
+
const o = this.optsWithGlobals();
|
|
972
|
+
const audienceIds = String(o.audience || "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
973
|
+
if (!audienceIds.length) throw new Error("--audience is required (comma-separated ids)");
|
|
974
|
+
await confirmDestructive(this, o.at
|
|
975
|
+
? `Schedule newsletter ${id} for ${o.at} to ${audienceIds.length} audience(s).`
|
|
976
|
+
: `Send newsletter ${id} now to ${audienceIds.length} audience(s).`);
|
|
977
|
+
const r = await call("newsletter", "send", {
|
|
978
|
+
id, audience_ids: audienceIds, mode: o.at ? "schedule" : "now",
|
|
979
|
+
scheduled_for: o.at, from: o.from,
|
|
980
|
+
}, ctxFrom(this));
|
|
981
|
+
emit(this, r, (d) => process.stdout.write(`\u2714 ${d.mode === "schedule" ? "Scheduled" : "Sent"} ${id}\n`));
|
|
982
|
+
} catch (e) { fail(e); }
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
withCommonOpts(nl.command("test <id> <email>"))
|
|
986
|
+
.description("Send a test copy of a newsletter to one address")
|
|
987
|
+
.action(async function (id, email) {
|
|
988
|
+
try {
|
|
989
|
+
const r = await call("newsletter", "send_test", { id, test_email: email }, ctxFrom(this));
|
|
990
|
+
emit(this, r, () => process.stdout.write(`\u2714 Test sent to ${email}\n`));
|
|
991
|
+
} catch (e) { fail(e); }
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
withCommonOpts(nl.command("render <id>"))
|
|
995
|
+
.description("Render the final email HTML without sending")
|
|
996
|
+
.action(async function (id) {
|
|
997
|
+
try {
|
|
998
|
+
const r = await call("newsletter", "render", { id }, ctxFrom(this));
|
|
999
|
+
emit(this, r, (d) => process.stdout.write(
|
|
1000
|
+
(d.result?.html || d.result?.body_html || JSON.stringify(d.result, null, 2)) + "\n",
|
|
1001
|
+
));
|
|
1002
|
+
} catch (e) { fail(e); }
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
withCommonOpts(nl.command("metrics"))
|
|
1006
|
+
.description("Delivery metrics and sending health for the workspace")
|
|
1007
|
+
.option("--id <newsletterId>", "restrict to one newsletter")
|
|
1008
|
+
.option("--limit <n>", "events to scan", "500")
|
|
1009
|
+
.action(async function () {
|
|
1010
|
+
try {
|
|
1011
|
+
const o = this.optsWithGlobals();
|
|
1012
|
+
const r = await call("newsletter", "metrics", { id: o.id, limit: Number(o.limit) }, ctxFrom(this));
|
|
1013
|
+
emit(this, r, (d) => {
|
|
1014
|
+
const t = d.totals || {};
|
|
1015
|
+
const s = d.send_state;
|
|
1016
|
+
process.stdout.write(
|
|
1017
|
+
Object.keys(t).length
|
|
1018
|
+
? table(["event", "count"], Object.entries(t).map(([k, v]) => [k, String(v)])) + "\n"
|
|
1019
|
+
: "No delivery events yet\n",
|
|
1020
|
+
);
|
|
1021
|
+
if (d.rates) {
|
|
1022
|
+
process.stdout.write(`Open ${d.rates.open_rate}% Click ${d.rates.click_rate}% Bounce ${d.rates.bounce_rate}%\n`);
|
|
1023
|
+
}
|
|
1024
|
+
if (s) {
|
|
1025
|
+
process.stdout.write(
|
|
1026
|
+
`Daily ${s.daily_sent}/${s.daily_cap} warmup stage ${s.warmup_stage}` +
|
|
1027
|
+
(s.paused_reason ? ` PAUSED: ${s.paused_reason}` : "") + "\n",
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
} catch (e) { fail(e); }
|
|
1032
|
+
});
|
|
743
1033
|
}
|
|
744
1034
|
|
|
745
1035
|
export function registerVideo(program) {
|
|
@@ -782,6 +1072,115 @@ export function registerVideo(program) {
|
|
|
782
1072
|
emit(this, r, (d) => process.stdout.write(`${d.share_url ?? "not shared"}\n`));
|
|
783
1073
|
} catch (e) { fail(e); }
|
|
784
1074
|
});
|
|
1075
|
+
|
|
1076
|
+
withCommonOpts(v.command("avatars"))
|
|
1077
|
+
.description("List the avatars curated for this workspace")
|
|
1078
|
+
.action(async function () {
|
|
1079
|
+
try {
|
|
1080
|
+
const r = await call("video", "avatars", {}, ctxFrom(this));
|
|
1081
|
+
emit(this, r, (d) => {
|
|
1082
|
+
process.stdout.write(table(
|
|
1083
|
+
["avatar_id", "name", "gender", "voice", "default"],
|
|
1084
|
+
(d.avatars || []).map((x) => [
|
|
1085
|
+
truncate(x.avatar_id, 20), truncate(x.avatar_name, 24), n(x.gender),
|
|
1086
|
+
truncate(n(x.default_voice_id), 20), x.is_default ? "yes" : "",
|
|
1087
|
+
]),
|
|
1088
|
+
) + `\n${d.avatars?.length ?? 0} rows\n`);
|
|
1089
|
+
if (d.note) process.stdout.write(`${d.note}\n`);
|
|
1090
|
+
});
|
|
1091
|
+
} catch (e) { fail(e); }
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
withCommonOpts(v.command("create"))
|
|
1095
|
+
.description("Generate a new AI video (talking head or explainer)")
|
|
1096
|
+
.option("--prompt <text>", "brief or script context")
|
|
1097
|
+
.option("--source-url <url>", "generate an explainer from a web page")
|
|
1098
|
+
.option("--content-item <id>", "generate from a content item")
|
|
1099
|
+
.option("--kind <k>", "talking_head | explainer", "talking_head")
|
|
1100
|
+
.option("--avatar <id>", "avatar_id (see `video avatars`)")
|
|
1101
|
+
.option("--voice <id>", "voice_id")
|
|
1102
|
+
.option("--style <s>", "professional | casual | educational | inspirational")
|
|
1103
|
+
.option("--format <f>", "platform format (e.g. linkedin, shorts)")
|
|
1104
|
+
.option("--duration <n>", "target duration in seconds")
|
|
1105
|
+
.option("--script-mode <m>", "verbatim to use --prompt as the exact script")
|
|
1106
|
+
.action(async function () {
|
|
1107
|
+
try {
|
|
1108
|
+
const o = this.optsWithGlobals();
|
|
1109
|
+
if (!o.prompt && !o.sourceUrl && !o.contentItem) {
|
|
1110
|
+
throw new Error("Provide --prompt, --source-url or --content-item");
|
|
1111
|
+
}
|
|
1112
|
+
const r = await call("video", "create", {
|
|
1113
|
+
prompt: o.prompt,
|
|
1114
|
+
source_url: o.sourceUrl,
|
|
1115
|
+
content_item_id: o.contentItem,
|
|
1116
|
+
video_kind: o.kind,
|
|
1117
|
+
avatar_id: o.avatar,
|
|
1118
|
+
voice_id: o.voice,
|
|
1119
|
+
style: o.style,
|
|
1120
|
+
video_format: o.format,
|
|
1121
|
+
target_duration: o.duration ? Number(o.duration) : undefined,
|
|
1122
|
+
script_mode: o.scriptMode,
|
|
1123
|
+
}, ctxFrom(this));
|
|
1124
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video ?? d, null, 2) + "\n"));
|
|
1125
|
+
} catch (e) { fail(e); }
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
withCommonOpts(v.command("update <id>"))
|
|
1129
|
+
.description("Update video metadata (title, episode label, CTA)")
|
|
1130
|
+
.option("--title <t>")
|
|
1131
|
+
.option("--episode <label>")
|
|
1132
|
+
.option("--cta-type <t>", "book_call | visit_site | subscribe | custom | none")
|
|
1133
|
+
.option("--cta-label <l>")
|
|
1134
|
+
.option("--cta-url <u>")
|
|
1135
|
+
.option("--summary <s>")
|
|
1136
|
+
.action(async function (id) {
|
|
1137
|
+
try {
|
|
1138
|
+
const o = this.optsWithGlobals();
|
|
1139
|
+
const r = await call("video", "update", {
|
|
1140
|
+
id, title: o.title, episode_label: o.episode,
|
|
1141
|
+
cta_type: o.ctaType, cta_label: o.ctaLabel, cta_url: o.ctaUrl, summary: o.summary,
|
|
1142
|
+
}, ctxFrom(this));
|
|
1143
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video, null, 2) + "\n"));
|
|
1144
|
+
} catch (e) { fail(e); }
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
withCommonOpts(v.command("publish <id>"))
|
|
1148
|
+
.description("Make the video publicly shareable")
|
|
1149
|
+
.action(async function (id) {
|
|
1150
|
+
try {
|
|
1151
|
+
const r = await call("video", "publish", { id }, ctxFrom(this));
|
|
1152
|
+
emit(this, r, (d) => process.stdout.write(`${d.share_url ?? "published"}\n`));
|
|
1153
|
+
} catch (e) { fail(e); }
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
withCommonOpts(v.command("unpublish <id>"))
|
|
1157
|
+
.description("Revoke the public share link")
|
|
1158
|
+
.action(async function (id) {
|
|
1159
|
+
try {
|
|
1160
|
+
const r = await call("video", "unpublish", { id }, ctxFrom(this));
|
|
1161
|
+
emit(this, r, () => process.stdout.write("unpublished\n"));
|
|
1162
|
+
} catch (e) { fail(e); }
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
withCommonOpts(v.command("refresh <id>"))
|
|
1166
|
+
.description("Re-check the render status with the provider")
|
|
1167
|
+
.action(async function (id) {
|
|
1168
|
+
try {
|
|
1169
|
+
const r = await call("video", "refresh", { id }, ctxFrom(this));
|
|
1170
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video ?? d, null, 2) + "\n"));
|
|
1171
|
+
} catch (e) { fail(e); }
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
withCommonOpts(v.command("delete <id>"))
|
|
1175
|
+
.description("Delete a video project")
|
|
1176
|
+
.option("--yes", "skip confirmation")
|
|
1177
|
+
.action(async function (id) {
|
|
1178
|
+
try {
|
|
1179
|
+
await confirmDestructive(this, `Delete video project ${id}. This cannot be undone.`);
|
|
1180
|
+
const r = await call("video", "delete", { id }, ctxFrom(this));
|
|
1181
|
+
emit(this, r, (d) => process.stdout.write(`deleted ${d.deleted}\n`));
|
|
1182
|
+
} catch (e) { fail(e); }
|
|
1183
|
+
});
|
|
785
1184
|
}
|
|
786
1185
|
|
|
787
1186
|
export function registerWatchlist(program) {
|
|
@@ -950,6 +1349,64 @@ export function registerActivity(program) {
|
|
|
950
1349
|
});
|
|
951
1350
|
}
|
|
952
1351
|
|
|
1352
|
+
export function registerAudit(program) {
|
|
1353
|
+
const a = program.command("audit").description("Audit trail of CLI / agent / assistant actions");
|
|
1354
|
+
|
|
1355
|
+
withCommonOpts(a.command("list"))
|
|
1356
|
+
.description("List audited actions (newest first)")
|
|
1357
|
+
.option("--status <s>", "ok | denied | error")
|
|
1358
|
+
.option("--surface <s>", "cli | mcp | api | internal")
|
|
1359
|
+
.option("--resource <r>", "e.g. leads, campaigns, content")
|
|
1360
|
+
.option("--actor <email>", "filter by person")
|
|
1361
|
+
.option("--writes", "only actions that changed data")
|
|
1362
|
+
.option("--since <iso>", "ISO timestamp lower bound")
|
|
1363
|
+
.option("--until <iso>", "ISO timestamp upper bound")
|
|
1364
|
+
.option("--limit <n>", "max rows", "50")
|
|
1365
|
+
.action(async function () {
|
|
1366
|
+
try {
|
|
1367
|
+
const o = this.optsWithGlobals();
|
|
1368
|
+
const r = await call("audit", "list", {
|
|
1369
|
+
status: o.status, surface: o.surface, resource: o.resource,
|
|
1370
|
+
actor_email: o.actor, writes_only: !!o.writes,
|
|
1371
|
+
since: o.since, until: o.until, limit: Number(o.limit),
|
|
1372
|
+
}, ctxFrom(this));
|
|
1373
|
+
emit(this, r, (d) => {
|
|
1374
|
+
process.stdout.write(table(
|
|
1375
|
+
["when", "who", "from", "action", "status", "ms"],
|
|
1376
|
+
(d.entries || []).map((x) => [
|
|
1377
|
+
n(x.created_at).replace("T", " ").slice(0, 19),
|
|
1378
|
+
truncate(x.actor_email || x.agent_name || "-", 24),
|
|
1379
|
+
n(x.surface),
|
|
1380
|
+
truncate(`${x.resource}.${x.action}`, 28),
|
|
1381
|
+
n(x.status),
|
|
1382
|
+
n(x.duration_ms),
|
|
1383
|
+
]),
|
|
1384
|
+
) + `\n${d.entries?.length ?? 0} rows\n`);
|
|
1385
|
+
});
|
|
1386
|
+
} catch (e) { fail(e); }
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
withCommonOpts(a.command("show <id>"))
|
|
1390
|
+
.description("Full request and outcome for one audited action")
|
|
1391
|
+
.action(async function (id) {
|
|
1392
|
+
try {
|
|
1393
|
+
const r = await call("audit", "get", { id }, ctxFrom(this));
|
|
1394
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.entry, null, 2) + "\n"));
|
|
1395
|
+
} catch (e) { fail(e); }
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
withCommonOpts(a.command("stats"))
|
|
1399
|
+
.description("Rolled-up audit summary")
|
|
1400
|
+
.option("--since <iso>", "ISO timestamp lower bound")
|
|
1401
|
+
.action(async function () {
|
|
1402
|
+
try {
|
|
1403
|
+
const o = this.optsWithGlobals();
|
|
1404
|
+
const r = await call("audit", "stats", { since: o.since }, ctxFrom(this));
|
|
1405
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
1406
|
+
} catch (e) { fail(e); }
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
953
1410
|
export function registerWorkspacesExtras(program) {
|
|
954
1411
|
const w = program.command("workspaces").description("Workspaces (admin keys only)");
|
|
955
1412
|
|
|
@@ -1024,11 +1481,97 @@ export function registerIntegrations(program) {
|
|
|
1024
1481
|
});
|
|
1025
1482
|
|
|
1026
1483
|
withCommonOpts(i.command("check <platform>"))
|
|
1027
|
-
.description("Health-check one integration")
|
|
1484
|
+
.description("Health-check one integration (e.g. linkedin_tool, resend)")
|
|
1028
1485
|
.action(async function (platform) {
|
|
1029
1486
|
try {
|
|
1030
1487
|
const r = await call("integrations", "check", { platform }, ctxFrom(this));
|
|
1031
1488
|
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
1032
1489
|
} catch (e) { fail(e); }
|
|
1033
1490
|
});
|
|
1491
|
+
|
|
1492
|
+
withCommonOpts(i.command("health"))
|
|
1493
|
+
.description("Live verdict: is Resend configured and are LinkedIn seats connected?")
|
|
1494
|
+
.action(async function () {
|
|
1495
|
+
try {
|
|
1496
|
+
const r = await call("integrations", "health", {}, ctxFrom(this));
|
|
1497
|
+
emit(this, r, (d) => {
|
|
1498
|
+
const flag = d.healthy ? "✔ healthy" : "✖ issues: " + (d.issues || []).join(", ");
|
|
1499
|
+
process.stdout.write(`${flag}\n`);
|
|
1500
|
+
process.stdout.write(table(
|
|
1501
|
+
["integration", "state", "detail"],
|
|
1502
|
+
[
|
|
1503
|
+
["resend", d.resend?.state ?? "unknown",
|
|
1504
|
+
d.resend?.configured ? `from=${d.resend?.from_email ?? "?"}` : (d.resend?.detail ?? "not configured")],
|
|
1505
|
+
...(d.linkedin?.seats?.length
|
|
1506
|
+
? d.linkedin.seats.map((s) => [
|
|
1507
|
+
`linkedin:${truncate(s.display_name || s.account_id || "?", 22)}`,
|
|
1508
|
+
s.state, s.connection_status ?? "",
|
|
1509
|
+
])
|
|
1510
|
+
: [["linkedin", d.linkedin?.verdict ?? "unknown", "no seats"]]),
|
|
1511
|
+
],
|
|
1512
|
+
) + "\n");
|
|
1513
|
+
});
|
|
1514
|
+
} catch (e) { fail(e); }
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
export function registerReports(program) {
|
|
1519
|
+
const r = program.command("reports").description("Daily workspace reports (history + on-demand generation)");
|
|
1520
|
+
|
|
1521
|
+
withCommonOpts(r.command("list"))
|
|
1522
|
+
.description("List past daily reports (newest first)")
|
|
1523
|
+
.option("--since <date>", "report date lower bound (YYYY-MM-DD)")
|
|
1524
|
+
.option("--until <date>", "report date upper bound (YYYY-MM-DD)")
|
|
1525
|
+
.option("--limit <n>", "max rows", "25")
|
|
1526
|
+
.action(async function () {
|
|
1527
|
+
try {
|
|
1528
|
+
const o = this.optsWithGlobals();
|
|
1529
|
+
const res = await call("daily_reports", "list", {
|
|
1530
|
+
since: o.since, until: o.until, limit: Number(o.limit),
|
|
1531
|
+
}, ctxFrom(this));
|
|
1532
|
+
emit(this, res, (d) => {
|
|
1533
|
+
process.stdout.write(table(
|
|
1534
|
+
["date", "status", "activity", "recipients", "sent", "report id"],
|
|
1535
|
+
(d.reports || []).map((x) => [
|
|
1536
|
+
n(x.report_date), n(x.status), n(x.activity_total),
|
|
1537
|
+
n(x.recipient_count), n(x.sent_count), truncate(x.id, 13),
|
|
1538
|
+
]),
|
|
1539
|
+
) + `\n${d.reports?.length ?? 0} rows\n`);
|
|
1540
|
+
});
|
|
1541
|
+
} catch (e) { fail(e); }
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
withCommonOpts(r.command("show <idOrDate>"))
|
|
1545
|
+
.description("Show one daily report by id or date (YYYY-MM-DD)")
|
|
1546
|
+
.action(async function (idOrDate) {
|
|
1547
|
+
try {
|
|
1548
|
+
const isDate = /^\d{4}-\d{2}-\d{2}$/.test(idOrDate);
|
|
1549
|
+
const res = await call("daily_reports", "get", isDate ? { date: idOrDate } : { id: idOrDate }, ctxFrom(this));
|
|
1550
|
+
emit(this, res, (d) => {
|
|
1551
|
+
const x = d.report || {};
|
|
1552
|
+
process.stdout.write(`${x.london_date_label ?? x.report_date} — ${x.status}\n`);
|
|
1553
|
+
process.stdout.write(`activity: ${x.activity_total ?? 0} · recipients: ${x.recipient_count ?? 0} · sent: ${x.sent_count ?? 0}\n`);
|
|
1554
|
+
process.stdout.write(`url: ${x.report_url ?? "-"}\n`);
|
|
1555
|
+
process.stdout.write(JSON.stringify(x.stats ?? {}, null, 2) + "\n");
|
|
1556
|
+
});
|
|
1557
|
+
} catch (e) { fail(e); }
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
withCommonOpts(r.command("generate"))
|
|
1561
|
+
.description("Generate today's report now (dry-run; --send to email workspace members)")
|
|
1562
|
+
.option("--send", "actually email workspace members (default: generate only)")
|
|
1563
|
+
.option("--to <email>", "send only to this test address")
|
|
1564
|
+
.action(async function () {
|
|
1565
|
+
try {
|
|
1566
|
+
const o = this.optsWithGlobals();
|
|
1567
|
+
const res = await call("daily_reports", "generate", {
|
|
1568
|
+
send: !!o.send, test_to: o.to,
|
|
1569
|
+
}, ctxFrom(this));
|
|
1570
|
+
emit(this, res, (d) => {
|
|
1571
|
+
const x = d.report || {};
|
|
1572
|
+
process.stdout.write(`report ${x.report_id ?? x.id ?? "?"} — sent: ${d.sent ?? 0}\n`);
|
|
1573
|
+
if (x.report_url) process.stdout.write(`url: ${x.report_url}\n`);
|
|
1574
|
+
});
|
|
1575
|
+
} catch (e) { fail(e); }
|
|
1576
|
+
});
|
|
1034
1577
|
}
|