@series-a/cli 0.13.1 → 0.15.2
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 +25 -0
- package/package.json +1 -1
- package/src/commands/insights.mjs +351 -0
- package/src/index.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to this package are documented here. This project follows
|
|
|
4
4
|
[Semantic Versioning](https://semver.org/) and uses
|
|
5
5
|
[Changesets](https://github.com/changesets/changesets) to manage releases.
|
|
6
6
|
|
|
7
|
+
## 0.15.1 — Engine fixes and reliability patch
|
|
8
|
+
|
|
9
|
+
- `voice set` / `voice pin-benchmark` no longer fail with a `user_id` null violation — the Agent API now resolves the actor from the API key, workspace member, or owner context automatically.
|
|
10
|
+
- AI-generated invitations are truncated at sentence/word boundaries with `{{token}}` protection — no more messages cut off mid-placeholder or mid-URL.
|
|
11
|
+
- 1st-degree leads no longer stall sequences: the DM step is skipped with a clear `dm_skipped_not_first_degree` audit reason and the lead continues to the next step.
|
|
12
|
+
- Fixed misleading "completed" stats: leads that only had zero-read cached identity lookups are now finalized as `skipped` with an explicit finish reason instead of `completed`.
|
|
13
|
+
- `agent-run` 403s resolved: workspace guard now matches both agent name and type, so scheduled agents fire reliably from CLI-triggered workspaces.
|
|
14
|
+
- Newsletter metrics now fall back to send-time provider handoff for delivered counts when Resend webhooks are absent.
|
|
15
|
+
|
|
16
|
+
## 0.15.0 — Newsletter audiences and sending
|
|
17
|
+
|
|
18
|
+
- `advisoros audiences create|edit|delete|add-leads|remove-leads` — full CRUD for smart audiences (lead segments), not just read + enrol.
|
|
19
|
+
- `advisoros newsletter audience-create|audience-show|audience-edit|audience-delete` — manage mailing audiences; mirrored to Resend automatically when the workspace has it connected, local-only when it does not.
|
|
20
|
+
- `advisoros newsletter contact-add|contacts-import|contact-remove|contact-unsubscribe` — contact management including CSV bulk import.
|
|
21
|
+
- `advisoros newsletter sync-segment <segmentId> <audienceId>` — push a smart audience's leads into a mailing audience.
|
|
22
|
+
- `advisoros newsletter send|test|render|metrics` — send now, schedule, test-send, render HTML, and read delivery totals, open/click/bounce rates and sending health.
|
|
23
|
+
- New Agent API actions on `audiences` and `newsletter`; MCP gains `manage_audience` and `send_newsletter` (44 tools, manifest 0.12.0).
|
|
24
|
+
|
|
25
|
+
## 0.14.0 — Daily reports
|
|
26
|
+
|
|
27
|
+
- `advisoros reports list` — browse persisted daily reports (past and present) per workspace, with status, activity totals and recipient/sent counts.
|
|
28
|
+
- `advisoros reports show <id|YYYY-MM-DD>` — full report detail including stats and the shareable dashboard URL.
|
|
29
|
+
- `advisoros reports generate` — run the daily-report engine on demand; dry-run by default, `--send` emails all workspace members, `--to <email>` sends a single test copy.
|
|
30
|
+
- Backed by the new Agent API `daily_reports` resource (`list` / `get` / `generate`).
|
|
31
|
+
|
|
7
32
|
## 0.13.0 — Full lifecycle + scheduling engine
|
|
8
33
|
|
|
9
34
|
- Campaign lifecycle is complete from the terminal: `campaigns update` (name, goal, sequence, sender accounts, daily send limit — no longer create-time-only), `launch`, `pause`, `resume`, `archive`, `clone`, and `delete` (dry-run preview by default, `--force` to execute, blocked when the campaign has dispatched activity unless forced).
|
package/package.json
CHANGED
|
@@ -4,6 +4,35 @@ import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructiv
|
|
|
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) {
|
|
@@ -1224,3 +1514,64 @@ export function registerIntegrations(program) {
|
|
|
1224
1514
|
} catch (e) { fail(e); }
|
|
1225
1515
|
});
|
|
1226
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
|
+
});
|
|
1577
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
registerCampaignAnalytics, registerAnalytics, registerSafety, registerCtas,
|
|
14
14
|
registerNotifications, registerAudiences, registerNewsletter, registerVideo,
|
|
15
15
|
registerWatchlist, registerMeetings, registerActivity, registerWorkspacesExtras,
|
|
16
|
-
registerExport, registerIntegrations, registerAudit,
|
|
16
|
+
registerExport, registerIntegrations, registerAudit, registerReports,
|
|
17
17
|
} from "./commands/insights.mjs";
|
|
18
18
|
import { registerFlow } from "./commands/flow.mjs";
|
|
19
19
|
import { registerVaultSync } from "./commands/knowledge-sync.mjs";
|
|
@@ -62,6 +62,7 @@ registerWorkspacesExtras(program);
|
|
|
62
62
|
registerExport(program);
|
|
63
63
|
|
|
64
64
|
registerVaultSync(program);
|
|
65
|
+
registerReports(program);
|
|
65
66
|
|
|
66
67
|
registerFlow(program);
|
|
67
68
|
|