@audienti/cli 0.1.5 → 0.1.7
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 +22 -0
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/api-client.js +4 -0
- package/src/cli.js +1185 -104
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { ApiError, AudientiClient, DEFAULT_HOST, normalizeHost } from "./api-client.js";
|
|
4
5
|
import { configPath, deleteConfig, maskToken, readConfig, writeConfig } from "./config.js";
|
|
5
6
|
|
|
@@ -28,6 +29,28 @@ const PROSPECTS_ADD_NOTE_USAGE = "Usage: audienti prospects add-note <prsp_id> (
|
|
|
28
29
|
const PROSPECTS_ADD_STEER_USAGE = "Usage: audienti prospects add-steer <prsp_id> (--message <text> [--engagement-type <key>] | --payload <file.json>) [--json] [--account <acct_id>]";
|
|
29
30
|
const PROSPECTS_ADD_PROFILE_USAGE = "Usage: audienti prospects add-profile <prsp_id> --url <profile_url|email|phone> [--json] [--account <acct_id>]";
|
|
30
31
|
const PROSPECTS_REPORT_BAD_PROFILE_USAGE = "Usage: audienti prospects report-bad-profile <prsp_id> <prof_id|citation_id> [--json] [--account <acct_id>]";
|
|
32
|
+
const WRITER_TEST_RUN_USAGE = "Usage: audienti writer test-run <prsp_id> [--json] [--mode <report|plan|step>] [--branch <both|no-accept|accepted>] [--step <step_key|row_number>] [--no-cache] [--clear-cache] [--account <acct_id>]";
|
|
33
|
+
const MOTIONS_ANALYTICS_USAGE = "Usage: audienti motions analytics <motn_id> [--window 30d] [--json] [--account <acct_id>]";
|
|
34
|
+
const ANALYTICS_PROSPECTS_USAGE = "Usage: audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
35
|
+
const ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE = "Usage: audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
36
|
+
const ANALYTICS_USERS_USAGE = "Usage: audienti analytics users [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--provenance <source>] [--platform <linkedin|email|gmail>] [--json] [--account <acct_id>]";
|
|
37
|
+
const COHORT_STAGE_ORDER = [
|
|
38
|
+
"identified",
|
|
39
|
+
"pre_connect",
|
|
40
|
+
"connect_request",
|
|
41
|
+
"connected",
|
|
42
|
+
"engaged",
|
|
43
|
+
"meeting_requested",
|
|
44
|
+
"meeting_outcome_accepted",
|
|
45
|
+
"meeting_outcome_declined",
|
|
46
|
+
"nurture",
|
|
47
|
+
"non_responsive",
|
|
48
|
+
"delayed",
|
|
49
|
+
"rejected",
|
|
50
|
+
"cancel"
|
|
51
|
+
];
|
|
52
|
+
const DAY_OF_WEEK_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
53
|
+
const WRITER_TEST_RUN_CACHE_VERSION = 1;
|
|
31
54
|
const SEQUENCE_EXPORT_CSV_COLUMNS = [
|
|
32
55
|
"prospect_id",
|
|
33
56
|
"prospect_name",
|
|
@@ -56,7 +79,9 @@ const SEQUENCE_EXPORT_CSV_COLUMNS = [
|
|
|
56
79
|
export async function run(argv = process.argv.slice(2), deps = {}) {
|
|
57
80
|
const context = {
|
|
58
81
|
env: deps.env || process.env,
|
|
82
|
+
cwd: deps.cwd || process.cwd(),
|
|
59
83
|
fetchImpl: deps.fetch || globalThis.fetch,
|
|
84
|
+
now: deps.now || (() => new Date()),
|
|
60
85
|
sleep: deps.sleep || sleep,
|
|
61
86
|
stdout: deps.stdout || process.stdout,
|
|
62
87
|
stderr: deps.stderr || process.stderr
|
|
@@ -106,6 +131,7 @@ async function dispatch(argv, context) {
|
|
|
106
131
|
if (normalizedResource === "motions" && action === "list") return motionsList(rest, context, { accountOverride });
|
|
107
132
|
if (normalizedResource === "motions" && action === "show") return motionsShow(rest, context, { accountOverride });
|
|
108
133
|
if (normalizedResource === "motions" && action === "status") return motionsStatus(rest, context, { accountOverride });
|
|
134
|
+
if (normalizedResource === "motions" && action === "analytics") return motionsAnalytics(rest, context, { accountOverride });
|
|
109
135
|
if (normalizedResource === "motions" && action === "prospects") return motionsProspects(rest, context, { accountOverride });
|
|
110
136
|
if (normalizedResource === "motions" && action === "add-prospects") return motionsAddProspects(rest, context, { accountOverride });
|
|
111
137
|
if (normalizedResource === "motions" && action === "create") return motionsCreate(rest, context, { accountOverride });
|
|
@@ -122,11 +148,13 @@ async function dispatch(argv, context) {
|
|
|
122
148
|
if (normalizedResource === "prospects" && action === "sequence-export") return prospectsSequenceExport(rest, context, { accountOverride });
|
|
123
149
|
if (normalizedResource === "prospects" && action === "import") return prospectsImport(rest, context, { accountOverride });
|
|
124
150
|
if (normalizedResource === "prospects" && action === "import-status") return prospectsImportStatus(rest, context, { accountOverride });
|
|
151
|
+
if (normalizedResource === "writer" && action === "test-run") return writerTestRun(rest, context, { accountOverride });
|
|
125
152
|
if (normalizedResource === "tools" && action === "get") return toolsGet(rest, context, { accountOverride });
|
|
126
153
|
if (normalizedResource === "operator" && action === "queue") return operatorQueue(rest, context, { accountOverride });
|
|
127
154
|
if (normalizedResource === "operator" && action === "next") return operatorNext(rest, context, { accountOverride });
|
|
128
155
|
if (normalizedResource === "operator" && action === "outcome") return operatorOutcome(rest, context, { accountOverride });
|
|
129
156
|
if (normalizedResource === "analytics" && ["prospects", "prospect"].includes(action)) return analyticsProspects(rest, context, { accountOverride });
|
|
157
|
+
if (normalizedResource === "analytics" && ["users", "user"].includes(action)) return analyticsUsers(rest, context, { accountOverride });
|
|
130
158
|
if (normalizedResource === "analytics" && ["visibility", "visops"].includes(action)) return analyticsVisibility(rest, context, { accountOverride });
|
|
131
159
|
if (normalizedResource === "analytics" && action === "content") return analyticsContent(rest, context, { accountOverride });
|
|
132
160
|
|
|
@@ -172,11 +200,13 @@ function helpTopicFromArgs(args) {
|
|
|
172
200
|
function normalizeTopicParts(parts) {
|
|
173
201
|
if (parts[0] === "plays") return ["motions", ...parts.slice(1)];
|
|
174
202
|
if (parts[0] === "principals") return ["users", ...parts.slice(1)];
|
|
203
|
+
if (parts[0] === "writers") return ["writer", ...parts.slice(1)];
|
|
175
204
|
return parts;
|
|
176
205
|
}
|
|
177
206
|
|
|
178
207
|
function normalizeResource(resource) {
|
|
179
208
|
if (resource === "principals") return "users";
|
|
209
|
+
if (resource === "writers") return "writer";
|
|
180
210
|
return resource === "plays" ? "motions" : resource;
|
|
181
211
|
}
|
|
182
212
|
|
|
@@ -655,6 +685,23 @@ async function motionsStatus(args, context, { accountOverride } = {}) {
|
|
|
655
685
|
renderMotionStatus(status, context);
|
|
656
686
|
}
|
|
657
687
|
|
|
688
|
+
async function motionsAnalytics(args, context, { accountOverride } = {}) {
|
|
689
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
690
|
+
...jsonOptions(),
|
|
691
|
+
window: { type: "string" }
|
|
692
|
+
});
|
|
693
|
+
if (positionals.length !== 1) throw new CommandError(MOTIONS_ANALYTICS_USAGE);
|
|
694
|
+
|
|
695
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
696
|
+
const payload = await client.analyticsProspects(accountId, {
|
|
697
|
+
motion_id: positionals[0],
|
|
698
|
+
window: values.window || "30d"
|
|
699
|
+
});
|
|
700
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
701
|
+
|
|
702
|
+
renderMotionAnalytics(payload, context);
|
|
703
|
+
}
|
|
704
|
+
|
|
658
705
|
async function motionsProspects(args, context, { accountOverride } = {}) {
|
|
659
706
|
const { values, positionals } = parseCommandArgs(args, {
|
|
660
707
|
...jsonOptions(),
|
|
@@ -914,13 +961,63 @@ async function prospectNoteCommand(args, context, { accountOverride, forcedType,
|
|
|
914
961
|
}
|
|
915
962
|
|
|
916
963
|
async function prospectsSequencePreview(args, context, { accountOverride } = {}) {
|
|
964
|
+
return runSequencePreviewCommand(args, context, {
|
|
965
|
+
accountOverride,
|
|
966
|
+
usageText: "Usage: audienti prospects sequence-preview <prsp_id> [--json] [--connection-state <state>] [--account <acct_id>]",
|
|
967
|
+
title: "Sequence preview"
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function writerTestRun(args, context, { accountOverride } = {}) {
|
|
972
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
973
|
+
...jsonOptions(),
|
|
974
|
+
branch: { type: "string" },
|
|
975
|
+
branches: { type: "string" },
|
|
976
|
+
mode: { type: "string" },
|
|
977
|
+
step: { type: "string" },
|
|
978
|
+
"angle-index": { type: "string" },
|
|
979
|
+
"no-cache": { type: "boolean" },
|
|
980
|
+
"clear-cache": { type: "boolean" }
|
|
981
|
+
});
|
|
982
|
+
if (positionals.length !== 1) throw new CommandError(WRITER_TEST_RUN_USAGE);
|
|
983
|
+
if (values.branch && values.branches) throw new CommandError("Choose one branch filter: use either --branch or --branches.");
|
|
984
|
+
const draftMode = normalizeWriterTestRunMode(values.mode);
|
|
985
|
+
if (draftMode === "target" && !values.step) throw new CommandError("Step mode requires --step <step_key|row_number>.");
|
|
986
|
+
if (draftMode === "target" && !values.branch && !values.branches) throw new CommandError("Step mode requires --branch <no-accept|accepted>.");
|
|
987
|
+
|
|
988
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
989
|
+
const prospectId = positionals[0];
|
|
990
|
+
const branchFilter = values.branches || values.branch || "both";
|
|
991
|
+
const useCache = ["plan", "target"].includes(draftMode) && !values["no-cache"];
|
|
992
|
+
if (useCache && values["clear-cache"]) await clearWriterTestRunCache(context, { accountId, prospectId });
|
|
993
|
+
const cache = useCache ? await loadWriterTestRunCache(context, { accountId, prospectId }) : emptyWriterTestRunCache(context, { accountId, prospectId });
|
|
994
|
+
const cachedDrafts = useCache ? writerCachedDraftsForRequest(cache, branchFilter) : [];
|
|
995
|
+
|
|
996
|
+
const payload = await client.prospectSequenceExport(accountId, prospectId, compactObject({
|
|
997
|
+
branches: branchFilter,
|
|
998
|
+
angle_index: values["angle-index"],
|
|
999
|
+
draft_mode: draftMode,
|
|
1000
|
+
target_step: values.step,
|
|
1001
|
+
cached_drafts: cachedDrafts.length ? cachedDrafts : undefined
|
|
1002
|
+
}));
|
|
1003
|
+
if (useCache) {
|
|
1004
|
+
payload.meta ||= {};
|
|
1005
|
+
payload.meta.cache = writerCacheMeta(cache, cachedDrafts);
|
|
1006
|
+
await persistWriterDraftsFromPayload(context, { cache, payload });
|
|
1007
|
+
}
|
|
1008
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
1009
|
+
|
|
1010
|
+
renderWriterTestRun(payload, context);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function runSequencePreviewCommand(args, context, { accountOverride, usageText, title }) {
|
|
917
1014
|
const { values, positionals } = parseCommandArgs(args, {
|
|
918
1015
|
...jsonOptions(),
|
|
919
1016
|
"connection-state": { type: "string" }
|
|
920
1017
|
});
|
|
921
1018
|
|
|
922
1019
|
if (positionals.length !== 1) {
|
|
923
|
-
throw new CommandError(
|
|
1020
|
+
throw new CommandError(usageText);
|
|
924
1021
|
}
|
|
925
1022
|
|
|
926
1023
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
@@ -929,7 +1026,7 @@ async function prospectsSequencePreview(args, context, { accountOverride } = {})
|
|
|
929
1026
|
}));
|
|
930
1027
|
if (values.json) return writeJson(context.stdout, payload);
|
|
931
1028
|
|
|
932
|
-
renderProspectSequencePreview(payload, context);
|
|
1029
|
+
renderProspectSequencePreview(payload, context, { title });
|
|
933
1030
|
}
|
|
934
1031
|
|
|
935
1032
|
async function prospectsSequenceExport(args, context, { accountOverride } = {}) {
|
|
@@ -938,10 +1035,12 @@ async function prospectsSequenceExport(args, context, { accountOverride } = {})
|
|
|
938
1035
|
csv: { type: "boolean" },
|
|
939
1036
|
branch: { type: "string" },
|
|
940
1037
|
branches: { type: "string" },
|
|
1038
|
+
"draft-mode": { type: "string" },
|
|
1039
|
+
"target-step": { type: "string" },
|
|
941
1040
|
"angle-index": { type: "string" }
|
|
942
1041
|
});
|
|
943
1042
|
if (positionals.length !== 1) {
|
|
944
|
-
throw new CommandError("Usage: audienti prospects sequence-export <prsp_id> [--json|--csv] [--branch <both|no-accept|accepted>] [--account <acct_id>]");
|
|
1043
|
+
throw new CommandError("Usage: audienti prospects sequence-export <prsp_id> [--json|--csv] [--branch <both|no-accept|accepted>] [--draft-mode <all|plan|target>] [--target-step <step_key|row_number>] [--account <acct_id>]");
|
|
945
1044
|
}
|
|
946
1045
|
if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
|
|
947
1046
|
if (values.branch && values.branches) throw new CommandError("Choose one branch filter: use either --branch or --branches.");
|
|
@@ -949,7 +1048,9 @@ async function prospectsSequenceExport(args, context, { accountOverride } = {})
|
|
|
949
1048
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
950
1049
|
const payload = await client.prospectSequenceExport(accountId, positionals[0], compactObject({
|
|
951
1050
|
branches: values.branches || values.branch,
|
|
952
|
-
angle_index: values["angle-index"]
|
|
1051
|
+
angle_index: values["angle-index"],
|
|
1052
|
+
draft_mode: values["draft-mode"],
|
|
1053
|
+
target_step: values["target-step"]
|
|
953
1054
|
}));
|
|
954
1055
|
if (values.json) return writeJson(context.stdout, payload);
|
|
955
1056
|
if (values.csv) return writeLine(context.stdout, sequenceExportRowsToCsv(payload?.rows || []));
|
|
@@ -957,6 +1058,170 @@ async function prospectsSequenceExport(args, context, { accountOverride } = {})
|
|
|
957
1058
|
renderProspectSequenceExport(payload, context);
|
|
958
1059
|
}
|
|
959
1060
|
|
|
1061
|
+
function normalizeWriterTestRunMode(value) {
|
|
1062
|
+
const normalized = String(value || "report").trim().toLowerCase();
|
|
1063
|
+
if (["report", "draft", "drafts", "all", "full"].includes(normalized)) return "all";
|
|
1064
|
+
if (normalized === "plan") return "plan";
|
|
1065
|
+
if (["step", "target"].includes(normalized)) return "target";
|
|
1066
|
+
|
|
1067
|
+
throw new CommandError("Unsupported writer test-run mode. Use report, plan, or step.");
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function emptyWriterTestRunCache(context, { accountId, prospectId }) {
|
|
1071
|
+
return {
|
|
1072
|
+
version: WRITER_TEST_RUN_CACHE_VERSION,
|
|
1073
|
+
account_id: accountId,
|
|
1074
|
+
prospect_id: prospectId,
|
|
1075
|
+
path: writerTestRunCachePath(context, { accountId, prospectId }),
|
|
1076
|
+
entries: {}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function loadWriterTestRunCache(context, { accountId, prospectId }) {
|
|
1081
|
+
const cache = emptyWriterTestRunCache(context, { accountId, prospectId });
|
|
1082
|
+
|
|
1083
|
+
try {
|
|
1084
|
+
const parsed = JSON.parse(await readFile(cache.path, "utf8"));
|
|
1085
|
+
if (parsed?.version !== WRITER_TEST_RUN_CACHE_VERSION) return cache;
|
|
1086
|
+
|
|
1087
|
+
return {
|
|
1088
|
+
...cache,
|
|
1089
|
+
entries: parsed.entries && typeof parsed.entries === "object" ? parsed.entries : {}
|
|
1090
|
+
};
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (error.code === "ENOENT") return cache;
|
|
1093
|
+
if (error instanceof SyntaxError) return cache;
|
|
1094
|
+
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async function clearWriterTestRunCache(context, { accountId, prospectId }) {
|
|
1100
|
+
await rm(writerTestRunCachePath(context, { accountId, prospectId }), { force: true });
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function writerTestRunCachePath(context, { accountId, prospectId }) {
|
|
1104
|
+
const dir = context.env.AUDIENTI_WRITER_TEST_RUN_CACHE_DIR || join(context.cwd, "tmp", "writer-test-run-cache");
|
|
1105
|
+
return join(dir, `${safeCacheSegment(accountId)}-${safeCacheSegment(prospectId)}.json`);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function safeCacheSegment(value) {
|
|
1109
|
+
return String(value || "unknown").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function writerCachedDraftsForRequest(cache, branchFilter) {
|
|
1113
|
+
const branchKeys = writerRequestedBranchKeys(branchFilter);
|
|
1114
|
+
return Object.values(cache.entries || {})
|
|
1115
|
+
.filter((entry) => branchKeys.includes(entry.branch))
|
|
1116
|
+
.filter((entry) => entry.key && (entry.body || entry.text || entry.subject || writerCacheQualityFailure(entry)))
|
|
1117
|
+
.map((entry) => compactObject({
|
|
1118
|
+
branch: entry.branch,
|
|
1119
|
+
key: entry.key,
|
|
1120
|
+
stage: entry.stage,
|
|
1121
|
+
channel: entry.channel,
|
|
1122
|
+
platform: entry.platform,
|
|
1123
|
+
message_mode: entry.message_mode,
|
|
1124
|
+
subject: entry.subject,
|
|
1125
|
+
body: entry.body,
|
|
1126
|
+
text: entry.text,
|
|
1127
|
+
status: entry.status,
|
|
1128
|
+
quality_codes: entry.quality_codes,
|
|
1129
|
+
blank_reason: entry.blank_reason,
|
|
1130
|
+
writer_path: entry.writer_path,
|
|
1131
|
+
generated_at: entry.generated_at,
|
|
1132
|
+
writer_engine: entry.writer_engine,
|
|
1133
|
+
target: entry.target,
|
|
1134
|
+
metadata: entry.metadata
|
|
1135
|
+
}));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function writerRequestedBranchKeys(branchFilter) {
|
|
1139
|
+
const values = String(branchFilter || "both").split(",").map((value) => value.trim()).filter(Boolean);
|
|
1140
|
+
if (values.length === 0 || values.includes("both")) return ["no_accept", "accepted"];
|
|
1141
|
+
|
|
1142
|
+
return values.map((value) => {
|
|
1143
|
+
const normalized = value.replaceAll("-", "_");
|
|
1144
|
+
if (["default", "no_accept", "not_connected"].includes(normalized)) return "no_accept";
|
|
1145
|
+
if (normalized === "accepted") return "accepted";
|
|
1146
|
+
return normalized;
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function writerCacheMeta(cache, cachedDrafts) {
|
|
1151
|
+
return {
|
|
1152
|
+
enabled: true,
|
|
1153
|
+
path: cache.path,
|
|
1154
|
+
entry_count: Object.keys(cache.entries || {}).length,
|
|
1155
|
+
sent_draft_count: cachedDrafts.length
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function persistWriterDraftsFromPayload(context, { cache, payload }) {
|
|
1160
|
+
const entries = { ...(cache.entries || {}) };
|
|
1161
|
+
let changed = false;
|
|
1162
|
+
|
|
1163
|
+
for (const branch of Array.isArray(payload?.branches) ? payload.branches : []) {
|
|
1164
|
+
const branchKey = String(branch?.key || "").trim();
|
|
1165
|
+
if (!branchKey) continue;
|
|
1166
|
+
|
|
1167
|
+
for (const step of Array.isArray(branch?.steps) ? branch.steps : []) {
|
|
1168
|
+
const entry = writerCacheEntryFromStep(step, { branchKey, generatedAt: branch.generated_at || payload?.generated_at });
|
|
1169
|
+
if (!entry) continue;
|
|
1170
|
+
|
|
1171
|
+
entries[writerCacheEntryKey(entry)] = entry;
|
|
1172
|
+
changed = true;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (!changed) return;
|
|
1177
|
+
|
|
1178
|
+
const nextCache = {
|
|
1179
|
+
version: WRITER_TEST_RUN_CACHE_VERSION,
|
|
1180
|
+
account_id: cache.account_id,
|
|
1181
|
+
prospect_id: cache.prospect_id,
|
|
1182
|
+
updated_at: new Date().toISOString(),
|
|
1183
|
+
entries
|
|
1184
|
+
};
|
|
1185
|
+
await mkdir(dirname(cache.path), { recursive: true });
|
|
1186
|
+
await writeFile(cache.path, `${JSON.stringify(nextCache, null, 2)}\n`, "utf8");
|
|
1187
|
+
cache.entries = entries;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function writerCacheEntryFromStep(step, { branchKey, generatedAt }) {
|
|
1191
|
+
if (step?.kind !== "message") return null;
|
|
1192
|
+
if (!step.key) return null;
|
|
1193
|
+
if (!(step.body || step.text || step.subject || writerCacheQualityFailure(step))) return null;
|
|
1194
|
+
if (step.status === "planned" || step.status === "unavailable" || step.status === "error") return null;
|
|
1195
|
+
|
|
1196
|
+
return compactObject({
|
|
1197
|
+
branch: branchKey,
|
|
1198
|
+
key: step.key,
|
|
1199
|
+
stage: step.stage,
|
|
1200
|
+
channel: step.channel,
|
|
1201
|
+
platform: step.platform,
|
|
1202
|
+
message_mode: step.message_mode,
|
|
1203
|
+
subject: step.subject,
|
|
1204
|
+
body: step.body,
|
|
1205
|
+
text: step.text,
|
|
1206
|
+
status: step.status,
|
|
1207
|
+
quality_codes: Array.isArray(step.quality_codes) ? step.quality_codes.filter(Boolean) : undefined,
|
|
1208
|
+
blank_reason: step.blank_reason,
|
|
1209
|
+
writer_path: step.writer_path,
|
|
1210
|
+
generated_at: step.generated_at || generatedAt || new Date().toISOString(),
|
|
1211
|
+
writer_engine: step.writer_engine || step?.metadata?.writer_engine,
|
|
1212
|
+
target: step.target,
|
|
1213
|
+
metadata: step.metadata
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function writerCacheEntryKey(entry) {
|
|
1218
|
+
return `${entry.branch}:${entry.key}`;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function writerCacheQualityFailure(entry) {
|
|
1222
|
+
return String(entry?.status || "") === "quality_failure";
|
|
1223
|
+
}
|
|
1224
|
+
|
|
960
1225
|
async function prospectsImport(args, context, { accountOverride } = {}) {
|
|
961
1226
|
const { values, positionals } = parseCommandArgs(args, {
|
|
962
1227
|
...jsonOptions(),
|
|
@@ -1102,8 +1367,12 @@ async function operatorOutcome(args, context, { accountOverride } = {}) {
|
|
|
1102
1367
|
}
|
|
1103
1368
|
|
|
1104
1369
|
async function analyticsProspects(args, context, { accountOverride } = {}) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1370
|
+
if (args[0] === "cohort-analysis") {
|
|
1371
|
+
return analyticsProspectsCohortAnalysis(args.slice(1), context, { accountOverride });
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const { values, positionals } = parseCommandArgs(args, analyticsProspectsOptions());
|
|
1375
|
+
if (positionals.length > 0) throw new CommandError(ANALYTICS_PROSPECTS_USAGE);
|
|
1107
1376
|
|
|
1108
1377
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
1109
1378
|
const payload = await client.analyticsProspects(accountId, analyticsQuery(values));
|
|
@@ -1112,6 +1381,61 @@ async function analyticsProspects(args, context, { accountOverride } = {}) {
|
|
|
1112
1381
|
renderAnalyticsProspects(payload, context);
|
|
1113
1382
|
}
|
|
1114
1383
|
|
|
1384
|
+
async function analyticsProspectsCohortAnalysis(args, context, { accountOverride } = {}) {
|
|
1385
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
1386
|
+
...jsonOptions(),
|
|
1387
|
+
weeks: { type: "string" },
|
|
1388
|
+
window: { type: "string" },
|
|
1389
|
+
motion: { type: "string" },
|
|
1390
|
+
provenance: { type: "string" },
|
|
1391
|
+
user: { type: "string" }
|
|
1392
|
+
});
|
|
1393
|
+
if (positionals.length > 0) throw new CommandError(ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE);
|
|
1394
|
+
|
|
1395
|
+
const weeks = normalizedCohortAnalysisWeeks(values.weeks);
|
|
1396
|
+
const cohorts = weeklyCohorts({ weeks, now: currentDate(context) });
|
|
1397
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
1398
|
+
const rows = [];
|
|
1399
|
+
|
|
1400
|
+
for (const cohort of cohorts) {
|
|
1401
|
+
const payload = await client.analyticsProspects(accountId, compactObject({
|
|
1402
|
+
window: values.window,
|
|
1403
|
+
account_user_id: values.user,
|
|
1404
|
+
motion_id: values.motion,
|
|
1405
|
+
provenance: values.provenance,
|
|
1406
|
+
cohort_start: cohort.start_date,
|
|
1407
|
+
cohort_end: cohort.end_date
|
|
1408
|
+
}));
|
|
1409
|
+
rows.push(cohortAnalysisRow(payload, cohort));
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const payload = {
|
|
1413
|
+
kind: "prospect_cohort_analysis",
|
|
1414
|
+
weeks,
|
|
1415
|
+
window: values.window || "24h",
|
|
1416
|
+
motion: rows.find((row) => row.motion)?.motion || motionPayload(values.motion),
|
|
1417
|
+
provenance: rows.find((row) => row.provenance)?.provenance || provenancePayload(values.provenance),
|
|
1418
|
+
account_user: rows.find((row) => row.account_user)?.account_user || null,
|
|
1419
|
+
cohorts: rows
|
|
1420
|
+
};
|
|
1421
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
1422
|
+
|
|
1423
|
+
renderAnalyticsProspectCohortAnalysis(payload, context);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
async function analyticsUsers(args, context, { accountOverride } = {}) {
|
|
1427
|
+
const { values, positionals } = parseCommandArgs(args, analyticsUsersOptions());
|
|
1428
|
+
if (positionals.length > 0) throw new CommandError(ANALYTICS_USERS_USAGE);
|
|
1429
|
+
validateDatePair(values.start, values.end, "--start", "--end");
|
|
1430
|
+
validateDatePair(values["cohort-start"], values["cohort-end"], "--cohort-start", "--cohort-end");
|
|
1431
|
+
|
|
1432
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
1433
|
+
const payload = await client.analyticsUsers(accountId, analyticsUsersQuery(values));
|
|
1434
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
1435
|
+
|
|
1436
|
+
renderAnalyticsUsers(payload, context);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1115
1439
|
async function analyticsVisibility(args, context, { accountOverride } = {}) {
|
|
1116
1440
|
const { values, positionals } = parseCommandArgs(args, analyticsOptions());
|
|
1117
1441
|
if (positionals.length > 0) throw new CommandError("Usage: audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]");
|
|
@@ -1260,13 +1584,161 @@ function analyticsOptions() {
|
|
|
1260
1584
|
};
|
|
1261
1585
|
}
|
|
1262
1586
|
|
|
1587
|
+
function analyticsProspectsOptions() {
|
|
1588
|
+
return {
|
|
1589
|
+
...analyticsOptions(),
|
|
1590
|
+
"cohort-start": { type: "string" },
|
|
1591
|
+
"cohort-end": { type: "string" },
|
|
1592
|
+
motion: { type: "string" },
|
|
1593
|
+
provenance: { type: "string" }
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function analyticsUsersOptions() {
|
|
1598
|
+
return {
|
|
1599
|
+
...jsonOptions(),
|
|
1600
|
+
user: { type: "string" },
|
|
1601
|
+
window: { type: "string" },
|
|
1602
|
+
start: { type: "string" },
|
|
1603
|
+
end: { type: "string" },
|
|
1604
|
+
"cohort-start": { type: "string" },
|
|
1605
|
+
"cohort-end": { type: "string" },
|
|
1606
|
+
motion: { type: "string" },
|
|
1607
|
+
provenance: { type: "string" },
|
|
1608
|
+
platform: { type: "string" },
|
|
1609
|
+
channel: { type: "string" }
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1263
1613
|
function analyticsQuery(values) {
|
|
1264
1614
|
return compactObject({
|
|
1265
1615
|
window: values.window,
|
|
1616
|
+
cohort_start: values["cohort-start"],
|
|
1617
|
+
cohort_end: values["cohort-end"],
|
|
1618
|
+
motion_id: values.motion,
|
|
1619
|
+
provenance: values.provenance,
|
|
1266
1620
|
account_user_id: values.user
|
|
1267
1621
|
});
|
|
1268
1622
|
}
|
|
1269
1623
|
|
|
1624
|
+
function analyticsUsersQuery(values) {
|
|
1625
|
+
const hasDateRange = Boolean(values.start || values.end);
|
|
1626
|
+
return compactObject({
|
|
1627
|
+
account_user_id: values.user || "me",
|
|
1628
|
+
window: hasDateRange ? undefined : (values.window || "30d"),
|
|
1629
|
+
start_date: values.start,
|
|
1630
|
+
end_date: values.end,
|
|
1631
|
+
cohort_start: values["cohort-start"],
|
|
1632
|
+
cohort_end: values["cohort-end"],
|
|
1633
|
+
motion_id: values.motion,
|
|
1634
|
+
provenance: values.provenance,
|
|
1635
|
+
platform: values.platform || values.channel
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function validateDatePair(start, end, startFlag, endFlag) {
|
|
1640
|
+
if ((start && !end) || (!start && end)) {
|
|
1641
|
+
throw new CommandError(`${startFlag} and ${endFlag} must be provided together.`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function normalizedCohortAnalysisWeeks(rawValue) {
|
|
1646
|
+
const weeks = Number.parseInt(rawValue || "4", 10);
|
|
1647
|
+
if (!Number.isInteger(weeks) || weeks <= 0) {
|
|
1648
|
+
throw new CommandError("--weeks must be a positive integer.");
|
|
1649
|
+
}
|
|
1650
|
+
if (weeks > 26) {
|
|
1651
|
+
throw new CommandError("--weeks must be 26 or less.");
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
return weeks;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function currentDate(context) {
|
|
1658
|
+
const raw = typeof context.now === "function" ? context.now() : context.now;
|
|
1659
|
+
const date = raw instanceof Date ? raw : new Date(raw || Date.now());
|
|
1660
|
+
if (Number.isNaN(date.getTime())) return utcDateOnly(new Date());
|
|
1661
|
+
|
|
1662
|
+
return utcDateOnly(date);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function weeklyCohorts({ weeks, now }) {
|
|
1666
|
+
const currentWeekStart = startOfUtcWeek(now);
|
|
1667
|
+
const rows = [];
|
|
1668
|
+
|
|
1669
|
+
for (let offset = weeks - 1; offset >= 0; offset -= 1) {
|
|
1670
|
+
const start = addUtcDays(currentWeekStart, offset * -7);
|
|
1671
|
+
const plannedEnd = addUtcDays(start, 6);
|
|
1672
|
+
const end = plannedEnd > now ? now : plannedEnd;
|
|
1673
|
+
rows.push({
|
|
1674
|
+
start_date: isoDate(start),
|
|
1675
|
+
end_date: isoDate(end)
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return rows;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function startOfUtcWeek(date) {
|
|
1683
|
+
const day = date.getUTCDay();
|
|
1684
|
+
const mondayOffset = (day + 6) % 7;
|
|
1685
|
+
return addUtcDays(date, -mondayOffset);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function utcDateOnly(date) {
|
|
1689
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function addUtcDays(date, days) {
|
|
1693
|
+
const next = new Date(date.getTime());
|
|
1694
|
+
next.setUTCDate(next.getUTCDate() + days);
|
|
1695
|
+
return utcDateOnly(next);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function isoDate(date) {
|
|
1699
|
+
return date.toISOString().slice(0, 10);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function cohortAnalysisRow(payload, fallbackCohort) {
|
|
1703
|
+
const cohort = payload?.cohort || fallbackCohort;
|
|
1704
|
+
const stages = {};
|
|
1705
|
+
const stageLabels = {};
|
|
1706
|
+
for (const row of Array.isArray(payload?.queue_stages) ? payload.queue_stages : []) {
|
|
1707
|
+
const key = String(row?.key || "").trim();
|
|
1708
|
+
if (!key) continue;
|
|
1709
|
+
|
|
1710
|
+
stages[key] = row?.count || 0;
|
|
1711
|
+
stageLabels[key] = row?.label || key;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
return {
|
|
1715
|
+
cohort,
|
|
1716
|
+
label: `${display(cohort.start_date)} to ${display(cohort.end_date)}`,
|
|
1717
|
+
total_count: payload?.cohort_prospects_count ?? payload?.prospects_added_count ?? 0,
|
|
1718
|
+
motion: payload?.motion || null,
|
|
1719
|
+
provenance: payload?.provenance || null,
|
|
1720
|
+
account_user: payload?.account_user || null,
|
|
1721
|
+
stages,
|
|
1722
|
+
stage_labels: stageLabels
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function motionPayload(motionId) {
|
|
1727
|
+
if (!motionId) return null;
|
|
1728
|
+
|
|
1729
|
+
return { prefix_id: motionId, name: motionId };
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function provenancePayload(provenance) {
|
|
1733
|
+
if (!provenance) return null;
|
|
1734
|
+
|
|
1735
|
+
return {
|
|
1736
|
+
key: provenance,
|
|
1737
|
+
label: humanize(provenance),
|
|
1738
|
+
field: "account_prospects.intake_source"
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1270
1742
|
function compactObject(object) {
|
|
1271
1743
|
return Object.fromEntries(
|
|
1272
1744
|
Object.entries(object).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== "")
|
|
@@ -1585,10 +2057,16 @@ function renderBulkMutationResult(payload, context, { successLabel, zeroSuccessL
|
|
|
1585
2057
|
function renderMotions(motions, context) {
|
|
1586
2058
|
if (!Array.isArray(motions) || motions.length === 0) return writeLine(context.stdout, "No motions found.");
|
|
1587
2059
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2060
|
+
writeAlignedTable(context, ["MOTION ID", "STATUS", "KIND", "NAME"], motions.map(motionTableRow));
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function motionTableRow(motion) {
|
|
2064
|
+
return [
|
|
2065
|
+
display(motion?.prefix_id),
|
|
2066
|
+
display(motion?.status),
|
|
2067
|
+
display(motion?.kind),
|
|
2068
|
+
display(motion?.name)
|
|
2069
|
+
];
|
|
1592
2070
|
}
|
|
1593
2071
|
|
|
1594
2072
|
function renderMotion(motion, context) {
|
|
@@ -1773,7 +2251,7 @@ function renderProspectProfileMutation(payload, context, { action }) {
|
|
|
1773
2251
|
if (profile.url) writeLine(context.stdout, `URL: ${profile.url}`);
|
|
1774
2252
|
}
|
|
1775
2253
|
|
|
1776
|
-
function renderProspectSequencePreview(payload, context) {
|
|
2254
|
+
function renderProspectSequencePreview(payload, context, { title = "Sequence preview" } = {}) {
|
|
1777
2255
|
const prospect = payload?.prospect || {};
|
|
1778
2256
|
const report = payload?.report || {};
|
|
1779
2257
|
const preview = report?.last_preview || {};
|
|
@@ -1782,6 +2260,7 @@ function renderProspectSequencePreview(payload, context) {
|
|
|
1782
2260
|
const steps = Array.isArray(report?.steps) ? report.steps : [];
|
|
1783
2261
|
const contextInfo = payload?.context || {};
|
|
1784
2262
|
|
|
2263
|
+
writeLine(context.stdout, title);
|
|
1785
2264
|
writeLine(context.stdout, `Prospect: ${display(prospect.display_name || selected.prospect_name)} (${display(prospect.prefix_id || selected.prospect_id)})`);
|
|
1786
2265
|
if (contextInfo.source) writeLine(context.stdout, `Context: ${contextInfo.source}`);
|
|
1787
2266
|
if (contextInfo.message) writeLine(context.stdout, contextInfo.message);
|
|
@@ -1802,21 +2281,195 @@ function renderProspectSequencePreview(payload, context) {
|
|
|
1802
2281
|
writeLine(context.stdout, "");
|
|
1803
2282
|
writeLine(context.stdout, "Sequence:");
|
|
1804
2283
|
|
|
1805
|
-
steps.forEach((step, index) =>
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
});
|
|
2284
|
+
steps.forEach((step, index) => renderSequenceStep(step, index, context));
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function renderWriterTestRun(payload, context) {
|
|
2288
|
+
const prospect = payload?.prospect || {};
|
|
2289
|
+
const branches = Array.isArray(payload?.branches) ? payload.branches : [];
|
|
2290
|
+
const draftMode = payload?.meta?.draft_mode || "all";
|
|
2291
|
+
const targetStep = payload?.meta?.target_step;
|
|
2292
|
+
|
|
2293
|
+
writeLine(context.stdout, "Writer campaign simulator");
|
|
2294
|
+
writeLine(context.stdout, `Prospect: ${display(prospect.display_name || prospect.name)} (${display(prospect.prefix_id)})`);
|
|
2295
|
+
if (payload?.context?.source) writeLine(context.stdout, `Context: ${payload.context.source}`);
|
|
2296
|
+
if (payload?.context?.message) writeLine(context.stdout, payload.context.message);
|
|
2297
|
+
if (payload?.context?.motion_name) writeLine(context.stdout, `Motion: ${payload.context.motion_name}`);
|
|
2298
|
+
if (payload?.context?.agent_name) writeLine(context.stdout, `Agent: ${payload.context.agent_name}`);
|
|
2299
|
+
if (payload?.context?.offer_name) writeLine(context.stdout, `Offer: ${payload.context.offer_name}`);
|
|
2300
|
+
writeLine(context.stdout, `Mode: ${display(draftMode)}`);
|
|
2301
|
+
if (targetStep) writeLine(context.stdout, `Target step: ${display(targetStep)}`);
|
|
2302
|
+
if (payload?.meta?.cache?.enabled) {
|
|
2303
|
+
writeLine(context.stdout, `Cache: ${display(payload.meta.cache.path)}`);
|
|
2304
|
+
writeLine(context.stdout, `Cached drafts sent: ${display(payload.meta.cache.sent_draft_count || 0)}`);
|
|
2305
|
+
}
|
|
2306
|
+
writeLine(context.stdout, `Start: ${isoDate(currentDate(context))}`);
|
|
2307
|
+
writeLine(context.stdout, "DATE: step execution date; for WAIT rows, the wait clears on that date.");
|
|
2308
|
+
writeLine(context.stdout, "Scenario: simulate the full path if the prospect does not reply.");
|
|
2309
|
+
if (draftMode === "plan") {
|
|
2310
|
+
writeLine(context.stdout, "Drafts are skipped; this run only plans the path and context.");
|
|
2311
|
+
} else if (draftMode === "target") {
|
|
2312
|
+
writeLine(context.stdout, "Only the target step is drafted; later steps are omitted.");
|
|
2313
|
+
} else {
|
|
2314
|
+
writeLine(context.stdout, "This can take a while because the writer drafts every message step.");
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
if (branches.length === 0) {
|
|
2318
|
+
writeLine(context.stdout, "No simulator branches were generated.");
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
for (const branch of branches) {
|
|
2323
|
+
const steps = Array.isArray(branch.steps) ? branch.steps : [];
|
|
2324
|
+
const summary = branch.summary || {};
|
|
2325
|
+
writeLine(context.stdout, "");
|
|
2326
|
+
writeLine(context.stdout, `${display(branch.label)} (${display(branch.key)})`);
|
|
2327
|
+
if (summary.channel_sequence?.length) writeLine(context.stdout, `Channels: ${summary.channel_sequence.join(" -> ")}`);
|
|
2328
|
+
if (summary.total_duration_days !== undefined) writeLine(context.stdout, `Duration days: ${summary.total_duration_days}`);
|
|
2329
|
+
if (summary.terminal_disposition) writeLine(context.stdout, `Terminal disposition: ${summary.terminal_disposition}`);
|
|
2330
|
+
|
|
2331
|
+
if (steps.length === 0) {
|
|
2332
|
+
writeLine(context.stdout, "No steps.");
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
renderWriterStepTable(steps, context);
|
|
2337
|
+
if (draftMode === "target") renderWriterTargetDraft(branch, context);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function renderWriterStepTable(steps, context) {
|
|
2342
|
+
writeLine(context.stdout, "# DATE DOW TYPE ACTION CH STATUS");
|
|
2343
|
+
writeLine(context.stdout, "-- ---------- --- ---- ------------------------------------------ ------- ---------");
|
|
2344
|
+
steps.forEach((step, index) => renderWriterStepRow(step, index, context));
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
function renderWriterStepRow(step, index, context) {
|
|
2348
|
+
const stepDate = writerStepDate(step, context);
|
|
2349
|
+
const row = [
|
|
2350
|
+
fixedWidth(index + 1, 2, { align: "right" }),
|
|
2351
|
+
fixedWidth(stepDate, 10),
|
|
2352
|
+
fixedWidth(writerStepDow(stepDate), 3),
|
|
2353
|
+
fixedWidth(compactKindLabel(step.kind), 4),
|
|
2354
|
+
fixedWidth(writerStepAction(step), 42),
|
|
2355
|
+
fixedWidth(compactChannelLabel(step.channel), 7),
|
|
2356
|
+
fixedWidth(compactWriterStatusLabel(step.status), 9)
|
|
2357
|
+
].join(" ");
|
|
2358
|
+
writeLine(context.stdout, row);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function renderWriterTargetDraft(branch, context) {
|
|
2362
|
+
const steps = Array.isArray(branch.steps) ? branch.steps : [];
|
|
2363
|
+
const resolvedTargetStep = String(branch.resolved_target_step || "").trim();
|
|
2364
|
+
const draftedStep = steps.find((step) => step?.kind === "message" && step?.key === resolvedTargetStep) ||
|
|
2365
|
+
[...steps].reverse().find((step) => step?.kind === "message" && (step.body || step.text || step.subject));
|
|
2366
|
+
if (!draftedStep) return;
|
|
2367
|
+
|
|
2368
|
+
writeLine(context.stdout, "");
|
|
2369
|
+
writeLine(context.stdout, `Drafted copy: ${display(draftedStep.stage)}${branch.resolved_target_step ? ` (${branch.resolved_target_step})` : ""}`);
|
|
2370
|
+
const targetUrl = writerDraftTargetUrl(draftedStep);
|
|
2371
|
+
if (targetUrl) writeLine(context.stdout, `Replying to: ${targetUrl}`);
|
|
2372
|
+
if (draftedStep.status && draftedStep.status !== "success") writeLine(context.stdout, `Status: ${draftedStep.status}`);
|
|
2373
|
+
if (String(draftedStep.status || "") === "quality_failure") {
|
|
2374
|
+
const qualityCodes = Array.isArray(draftedStep.quality_codes) ? draftedStep.quality_codes.filter(Boolean) : [];
|
|
2375
|
+
if (qualityCodes.length) writeLine(context.stdout, `Quality failure: ${qualityCodes.join(", ")}`);
|
|
2376
|
+
if (draftedStep.blank_reason) writeLine(context.stdout, `Blank reason: ${draftedStep.blank_reason}`);
|
|
2377
|
+
if (draftedStep.writer_path) writeLine(context.stdout, `Writer path: ${draftedStep.writer_path}`);
|
|
2378
|
+
if (draftedStep.writer_engine) writeLine(context.stdout, `Writer engine: ${draftedStep.writer_engine}`);
|
|
2379
|
+
}
|
|
2380
|
+
for (const warning of Array.isArray(draftedStep.warnings) ? draftedStep.warnings : []) {
|
|
2381
|
+
if (warning) writeLine(context.stdout, `Warning: ${warning}`);
|
|
2382
|
+
}
|
|
2383
|
+
if (draftedStep.missing_reason) writeLine(context.stdout, `Missing reason: ${draftedStep.missing_reason}`);
|
|
2384
|
+
if (draftedStep.subject) writeLine(context.stdout, `Subject: ${draftedStep.subject}`);
|
|
2385
|
+
const body = draftedStep.body || draftedStep.text || draftedStep.empty_body_reason;
|
|
2386
|
+
writeLine(context.stdout, display(body || "No draft body returned."));
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function writerDraftTargetUrl(step) {
|
|
2390
|
+
const target = step?.target || {};
|
|
2391
|
+
return target.post_url || target.comment_url || target.url || null;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
function writerStepDate(step, context) {
|
|
2395
|
+
if (step?.timing?.mode === "scheduled") return dateOnlyLabel(step?.timing?.scheduled_for);
|
|
2396
|
+
return String(display(step.kind)).toLowerCase() === "terminal" ? "after" : isoDate(currentDate(context));
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function writerStepDow(dateLabel) {
|
|
2400
|
+
const date = parseDateOnlyLabel(dateLabel);
|
|
2401
|
+
return date ? DAY_OF_WEEK_LABELS[date.getUTCDay()] : "";
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function dateOnlyLabel(value) {
|
|
2405
|
+
const raw = String(display(value)).trim();
|
|
2406
|
+
return raw.match(/^\d{4}-\d{2}-\d{2}/)?.[0] || raw;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function parseDateOnlyLabel(value) {
|
|
2410
|
+
const match = String(display(value)).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
2411
|
+
if (!match) return null;
|
|
2412
|
+
|
|
2413
|
+
const [, year, month, day] = match;
|
|
2414
|
+
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function writerStepAction(step) {
|
|
2418
|
+
return display(step.stage || step.key);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function compactKindLabel(kind) {
|
|
2422
|
+
const value = String(display(kind)).toLowerCase();
|
|
2423
|
+
if (value === "message") return "MSG";
|
|
2424
|
+
if (value === "action") return "ACT";
|
|
2425
|
+
if (value === "event") return "EVT";
|
|
2426
|
+
if (value === "terminal") return "END";
|
|
2427
|
+
if (value === "wait") return "WAIT";
|
|
2428
|
+
return display(kind).toUpperCase();
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
function compactChannelLabel(channel) {
|
|
2432
|
+
const value = String(display(channel));
|
|
2433
|
+
if (value === "LinkedIn") return "LI";
|
|
2434
|
+
if (value === "LinkedIn InMail") return "InMail";
|
|
2435
|
+
if (value === "Timeline") return "Wait";
|
|
2436
|
+
if (value === "Disposition") return "Done";
|
|
2437
|
+
return value;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
function compactWriterStatusLabel(status) {
|
|
2441
|
+
const value = String(display(status));
|
|
2442
|
+
if (value === "quality_failure") return "quality";
|
|
2443
|
+
return value;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
function fixedWidth(value, width, options = {}) {
|
|
2447
|
+
const text = truncateCliText(value, width);
|
|
2448
|
+
return options.align === "right" ? text.padStart(width) : text.padEnd(width);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function truncateCliText(value, maxLength) {
|
|
2452
|
+
const text = String(display(value)).replace(/\s+/g, " ").trim();
|
|
2453
|
+
if (text.length <= maxLength) return text;
|
|
2454
|
+
return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function renderSequenceStep(step, index, context) {
|
|
2458
|
+
const kind = display(step.kind).toUpperCase();
|
|
2459
|
+
const stage = display(step.stage);
|
|
2460
|
+
const channel = display(step.channel);
|
|
2461
|
+
const timing = step?.timing?.mode === "scheduled" ? ` [scheduled ${display(step?.timing?.scheduled_for)}]` : "";
|
|
2462
|
+
writeLine(context.stdout, `${index + 1}. ${kind} | ${stage} | ${channel}${timing}`);
|
|
2463
|
+
|
|
2464
|
+
if (step.disposition) writeLine(context.stdout, ` Disposition: ${step.disposition}`);
|
|
2465
|
+
if (step.status) writeLine(context.stdout, ` Status: ${step.status}`);
|
|
2466
|
+
if (step.transition_label) writeLine(context.stdout, ` Transition: ${step.transition_label}`);
|
|
2467
|
+
if (step.rationale) writeLine(context.stdout, ` Why: ${step.rationale}`);
|
|
2468
|
+
if (step.guidance) writeLine(context.stdout, ` Guidance: ${step.guidance}`);
|
|
2469
|
+
if (step.subject) writeLine(context.stdout, ` Subject: ${step.subject}`);
|
|
2470
|
+
if (step.body) writeLine(context.stdout, ` Body: ${step.body}`);
|
|
2471
|
+
if (step.empty_body_reason) writeLine(context.stdout, ` Empty body reason: ${step.empty_body_reason}`);
|
|
2472
|
+
if (step.missing_reason) writeLine(context.stdout, ` Missing reason: ${step.missing_reason}`);
|
|
1820
2473
|
}
|
|
1821
2474
|
|
|
1822
2475
|
function renderProspectSequenceExport(payload, context) {
|
|
@@ -1899,17 +2552,13 @@ function renderOperatorQueue(payload, context) {
|
|
|
1899
2552
|
return;
|
|
1900
2553
|
}
|
|
1901
2554
|
|
|
1902
|
-
|
|
1903
|
-
for (const row of queue) {
|
|
1904
|
-
writeLine(context.stdout, operatorRowLine(row));
|
|
1905
|
-
}
|
|
2555
|
+
writeOperatorRows(context, queue);
|
|
1906
2556
|
}
|
|
1907
2557
|
|
|
1908
2558
|
function renderOperatorNext(row, context) {
|
|
1909
2559
|
if (!row) return writeLine(context.stdout, "No operator moves found.");
|
|
1910
2560
|
|
|
1911
|
-
|
|
1912
|
-
writeLine(context.stdout, operatorRowLine(row));
|
|
2561
|
+
writeOperatorRows(context, [row]);
|
|
1913
2562
|
}
|
|
1914
2563
|
|
|
1915
2564
|
function renderOperatorPlan(row, context) {
|
|
@@ -1981,11 +2630,72 @@ function renderOperatorOutcome(payload, context) {
|
|
|
1981
2630
|
|
|
1982
2631
|
function renderAnalyticsProspects(payload, context) {
|
|
1983
2632
|
writeLine(context.stdout, `Prospect analytics (${analyticsWindowLabel(payload)})`);
|
|
2633
|
+
writeAnalyticsCohort(payload, context);
|
|
1984
2634
|
writeAnalyticsScope(payload, context);
|
|
1985
|
-
|
|
2635
|
+
if (payload?.cohort) {
|
|
2636
|
+
writeLine(context.stdout, `Cohort prospects: ${display(payload?.cohort_prospects_count, payload?.prospects_added_count || 0)}`);
|
|
2637
|
+
} else {
|
|
2638
|
+
writeLine(context.stdout, `Prospects added: ${display(payload?.prospects_added_count, 0)}`);
|
|
2639
|
+
}
|
|
1986
2640
|
writeAnalyticsActionSummary(payload?.actions, context, "Actions");
|
|
1987
2641
|
writeCountTable(context, "Action breakdown", payload?.actions?.breakdown, ["ACTION", "COUNT", "AUTOMATED", "AUTO %"], actionBreakdownRow);
|
|
1988
|
-
writeCountTable(context, "Queue stages", payload?.queue_stages, ["STAGE", "COUNT"], countRow);
|
|
2642
|
+
writeCountTable(context, payload?.cohort ? "Current cohort stages" : "Queue stages", payload?.queue_stages, ["STAGE", "COUNT"], countRow);
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
function renderMotionAnalytics(payload, context) {
|
|
2646
|
+
writeLine(context.stdout, `Motion analytics (${analyticsWindowLabel(payload)})`);
|
|
2647
|
+
writeAnalyticsMotion(payload, context);
|
|
2648
|
+
if (payload?.motion?.created_at) writeLine(context.stdout, `Created: ${payload.motion.created_at}`);
|
|
2649
|
+
writeLine(context.stdout, `Prospects produced: ${display(payload?.prospects_added_count, 0)}`);
|
|
2650
|
+
writeCountTable(context, "Prospect cohorts by produced day", payload?.prospects_by_day, ["DATE", "PRODUCED", "ACTIVE", "ACTIVE %", "INACTIVE", "STAGES"], dailyProspectRow);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
function renderAnalyticsProspectCohortAnalysis(payload, context) {
|
|
2654
|
+
const cohorts = Array.isArray(payload?.cohorts) ? payload.cohorts : [];
|
|
2655
|
+
writeLine(context.stdout, `Prospect cohort analysis (${display(payload?.weeks, cohorts.length)} weeks)`);
|
|
2656
|
+
writeLine(context.stdout, `Activity window: ${display(payload?.window, "24h")}`);
|
|
2657
|
+
writeLine(context.stdout, "Cohorts: account_prospects.created_at, calendar weeks, oldest first");
|
|
2658
|
+
writeAnalyticsMotion(payload, context);
|
|
2659
|
+
writeAnalyticsProvenance(payload, context);
|
|
2660
|
+
if (payload?.account_user) {
|
|
2661
|
+
writeLine(context.stdout, `User: ${entityLabel(payload.account_user)}`);
|
|
2662
|
+
} else {
|
|
2663
|
+
writeLine(context.stdout, "User: all account users");
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
if (cohorts.length === 0) {
|
|
2667
|
+
writeLine(context.stdout, "No cohorts generated.");
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
const stageColumns = cohortAnalysisStageColumns(cohorts);
|
|
2672
|
+
const headers = ["COHORT", "TOTAL", ...stageColumns.map((column) => column.label)];
|
|
2673
|
+
const rows = cohorts.map((row) => [
|
|
2674
|
+
row.label,
|
|
2675
|
+
row.total_count,
|
|
2676
|
+
...stageColumns.map((column) => row.stages?.[column.key] || 0)
|
|
2677
|
+
]);
|
|
2678
|
+
|
|
2679
|
+
writeLine(context.stdout, "");
|
|
2680
|
+
writeAlignedTable(context, headers, rows, {
|
|
2681
|
+
numericColumns: headers.map((_, index) => index > 0)
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
function renderAnalyticsUsers(payload, context) {
|
|
2686
|
+
writeLine(context.stdout, `User analytics (${analyticsActivityLabel(payload)})`);
|
|
2687
|
+
writeAnalyticsCohort(payload, context);
|
|
2688
|
+
writeAnalyticsScope(payload, context);
|
|
2689
|
+
writeAnalyticsPlatform(payload, context);
|
|
2690
|
+
|
|
2691
|
+
const summary = payload?.summary || {};
|
|
2692
|
+
writeLine(context.stdout, `Actions: ${display(summary.total_count, 0)}`);
|
|
2693
|
+
writeLine(context.stdout, `Performed by you: ${display(summary.performed_by_user_count, 0)} (${percentageLabel(summary.performed_by_user_percentage)})`);
|
|
2694
|
+
writeLine(context.stdout, `Other humans: ${display(summary.performed_by_others_count, 0)} (${percentageLabel(summary.performed_by_others_percentage)})`);
|
|
2695
|
+
writeLine(context.stdout, `Agent: ${display(summary.agentic_count, 0)} (${percentageLabel(summary.agentic_percentage)})`);
|
|
2696
|
+
writeAnalyticsDailyActions(payload, context);
|
|
2697
|
+
writeCountTable(context, "Action mix", payload?.action_mix, ["ACTION", "COUNT", "%"], mixRow);
|
|
2698
|
+
writeCountTable(context, "Platform mix", payload?.platform_mix, ["PLATFORM", "COUNT", "%"], mixRow);
|
|
1989
2699
|
}
|
|
1990
2700
|
|
|
1991
2701
|
function renderAnalyticsVisibility(payload, context) {
|
|
@@ -2005,6 +2715,8 @@ function renderAnalyticsContent(payload, context) {
|
|
|
2005
2715
|
}
|
|
2006
2716
|
|
|
2007
2717
|
function writeAnalyticsScope(payload, context) {
|
|
2718
|
+
writeAnalyticsMotion(payload, context);
|
|
2719
|
+
writeAnalyticsProvenance(payload, context);
|
|
2008
2720
|
if (payload?.account_user) {
|
|
2009
2721
|
writeLine(context.stdout, `User: ${entityLabel(payload.account_user)}`);
|
|
2010
2722
|
} else {
|
|
@@ -2012,6 +2724,113 @@ function writeAnalyticsScope(payload, context) {
|
|
|
2012
2724
|
}
|
|
2013
2725
|
}
|
|
2014
2726
|
|
|
2727
|
+
function writeAnalyticsPlatform(payload, context) {
|
|
2728
|
+
if (!payload?.platform) return;
|
|
2729
|
+
|
|
2730
|
+
const label = display(payload.platform.label, payload.platform.key);
|
|
2731
|
+
const values = Array.isArray(payload.platform.values) ? payload.platform.values.filter(Boolean).join(", ") : display(payload.platform.key);
|
|
2732
|
+
writeLine(context.stdout, `Platform: ${label} (${display(payload.platform.field, "events.platform")}: ${values})`);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
function writeAnalyticsMotion(payload, context) {
|
|
2736
|
+
if (!payload?.motion) return;
|
|
2737
|
+
|
|
2738
|
+
writeLine(context.stdout, `Motion: ${entityLabel(payload.motion)}`);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function writeAnalyticsProvenance(payload, context) {
|
|
2742
|
+
if (!payload?.provenance) return;
|
|
2743
|
+
|
|
2744
|
+
writeLine(context.stdout, `Provenance: ${display(payload.provenance.label, payload.provenance.key)} (${display(payload.provenance.field, "account_prospects.intake_source")})`);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function writeAnalyticsCohort(payload, context) {
|
|
2748
|
+
const cohort = payload?.cohort;
|
|
2749
|
+
if (!cohort) return;
|
|
2750
|
+
|
|
2751
|
+
writeLine(context.stdout, `Cohort: ${display(cohort.start_date)} to ${display(cohort.end_date)} (${display(cohort.field, "account_prospects.created_at")})`);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function writeAnalyticsDailyActions(payload, context) {
|
|
2755
|
+
const dailyRows = Array.isArray(payload?.daily_actions) ? payload.daily_actions : [];
|
|
2756
|
+
writeLine(context.stdout, "");
|
|
2757
|
+
writeLine(context.stdout, "Actions by day");
|
|
2758
|
+
if (dailyRows.length === 0) return writeLine(context.stdout, "None");
|
|
2759
|
+
|
|
2760
|
+
const actionColumns = dailyActionColumns(dailyRows, payload?.action_mix);
|
|
2761
|
+
const headers = ["DATE", "TOTAL", ...actionColumns.map((column) => column.label)];
|
|
2762
|
+
const rows = dailyRows.map((row) => [
|
|
2763
|
+
row?.date,
|
|
2764
|
+
row?.total_count || 0,
|
|
2765
|
+
...actionColumns.map((column) => row?.actions?.[column.key] || 0)
|
|
2766
|
+
]);
|
|
2767
|
+
|
|
2768
|
+
writeAlignedTable(context, headers, rows, {
|
|
2769
|
+
numericColumns: headers.map((_, index) => index > 0)
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
function dailyActionColumns(dailyRows, actionMix) {
|
|
2774
|
+
const labels = {};
|
|
2775
|
+
const keys = [];
|
|
2776
|
+
for (const row of Array.isArray(actionMix) ? actionMix : []) {
|
|
2777
|
+
const key = String(row?.key || "").trim();
|
|
2778
|
+
if (!key) continue;
|
|
2779
|
+
if (!keys.includes(key)) keys.push(key);
|
|
2780
|
+
labels[key] = row?.label || key;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
for (const row of dailyRows) {
|
|
2784
|
+
for (const key of Object.keys(row?.actions || {})) {
|
|
2785
|
+
if (!keys.includes(key)) keys.push(key);
|
|
2786
|
+
labels[key] ||= key;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
return keys.slice(0, 6).map((key) => ({ key, label: compactActionLabel(key, labels[key] || key) }));
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
function compactActionLabel(key, label) {
|
|
2794
|
+
const labels = {
|
|
2795
|
+
"action.profile.connect_request_sent": "Connect sent",
|
|
2796
|
+
"action.profile.withdraw_connection": "Withdraw",
|
|
2797
|
+
"action.profile.follow": "Follow",
|
|
2798
|
+
"action.profile.view": "View",
|
|
2799
|
+
"action.profile.in_mail_message": "InMail",
|
|
2800
|
+
"action.post.comment": "Comment",
|
|
2801
|
+
"action.post.like": "Like",
|
|
2802
|
+
"messaging.message_sent": "Message",
|
|
2803
|
+
"messaging.email_sent": "Email",
|
|
2804
|
+
"action.meeting.requested": "Meeting req",
|
|
2805
|
+
"action.prospect.nurtured": "Nurtured",
|
|
2806
|
+
"action.prospect.motion_completed_no_outcome": "No outcome"
|
|
2807
|
+
};
|
|
2808
|
+
if (labels[key]) return labels[key];
|
|
2809
|
+
|
|
2810
|
+
const words = String(label || key).split(/\s+/).filter(Boolean);
|
|
2811
|
+
return words.length <= 2 ? words.join(" ") : words.slice(0, 2).join(" ");
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
function cohortAnalysisStageColumns(cohorts) {
|
|
2815
|
+
const labels = {};
|
|
2816
|
+
const keys = [];
|
|
2817
|
+
for (const cohort of cohorts) {
|
|
2818
|
+
for (const [key, label] of Object.entries(cohort.stage_labels || {})) {
|
|
2819
|
+
if (!keys.includes(key)) keys.push(key);
|
|
2820
|
+
labels[key] ||= label;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
return keys
|
|
2825
|
+
.sort((left, right) => cohortStageRank(left) - cohortStageRank(right) || left.localeCompare(right))
|
|
2826
|
+
.map((key) => ({ key, label: labels[key] || key }));
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
function cohortStageRank(key) {
|
|
2830
|
+
const index = COHORT_STAGE_ORDER.indexOf(String(key));
|
|
2831
|
+
return index === -1 ? COHORT_STAGE_ORDER.length : index;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2015
2834
|
function writeAnalyticsActionSummary(actions, context, label) {
|
|
2016
2835
|
const total = display(actions?.total_count, 0);
|
|
2017
2836
|
const automated = display(actions?.automated_count, 0);
|
|
@@ -2025,8 +2844,32 @@ function writeCountTable(context, title, rows, headers, mapRow) {
|
|
|
2025
2844
|
writeLine(context.stdout, title);
|
|
2026
2845
|
if (list.length === 0) return writeLine(context.stdout, "None");
|
|
2027
2846
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2847
|
+
writeAlignedTable(context, headers, list.map(mapRow));
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
function writeAlignedTable(context, headers, rows, options = {}) {
|
|
2851
|
+
const tableRows = [headers, ...rows].map((row) => row.map((value) => display(value)));
|
|
2852
|
+
const widths = headers.map((_, index) => Math.max(...tableRows.map((row) => visibleLength(row[index] || ""))));
|
|
2853
|
+
const numericColumns = options.numericColumns || headers.map((header, index) => index > 0 && numericHeader(header));
|
|
2854
|
+
|
|
2855
|
+
writeLine(context.stdout, formatAlignedRow(headers, widths, numericColumns));
|
|
2856
|
+
writeLine(context.stdout, widths.map((width) => "-".repeat(width)).join(" "));
|
|
2857
|
+
for (const row of rows) writeLine(context.stdout, formatAlignedRow(row, widths, numericColumns));
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
function numericHeader(header) {
|
|
2861
|
+
return ["COUNT", "AUTOMATED", "AUTO %", "TOTAL", "%", "PRODUCED", "ACTIVE", "ACTIVE %", "INACTIVE"].includes(String(header || "").toUpperCase());
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function formatAlignedRow(row, widths, numericColumns) {
|
|
2865
|
+
return row.map((value, index) => {
|
|
2866
|
+
const text = String(display(value));
|
|
2867
|
+
return numericColumns[index] ? text.padStart(widths[index]) : text.padEnd(widths[index]);
|
|
2868
|
+
}).join(" ");
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
function visibleLength(value) {
|
|
2872
|
+
return String(display(value)).length;
|
|
2030
2873
|
}
|
|
2031
2874
|
|
|
2032
2875
|
function actionBreakdownRow(row) {
|
|
@@ -2045,6 +2888,37 @@ function countRow(row) {
|
|
|
2045
2888
|
];
|
|
2046
2889
|
}
|
|
2047
2890
|
|
|
2891
|
+
function dailyProspectRow(row) {
|
|
2892
|
+
const count = Number(row?.count || 0);
|
|
2893
|
+
return [
|
|
2894
|
+
display(row?.date),
|
|
2895
|
+
countDash(row?.count),
|
|
2896
|
+
countDash(row?.active_count),
|
|
2897
|
+
count > 0 ? percentageLabel(row?.active_percentage) : "-",
|
|
2898
|
+
countDash(row?.inactive_count),
|
|
2899
|
+
stageSummary(row?.queue_stages)
|
|
2900
|
+
];
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
function countDash(value) {
|
|
2904
|
+
return Number(value || 0) === 0 ? "-" : display(value, 0);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
function stageSummary(rows) {
|
|
2908
|
+
const stages = Array.isArray(rows) ? rows.filter((row) => Number(row?.count || 0) > 0) : [];
|
|
2909
|
+
if (stages.length === 0) return "-";
|
|
2910
|
+
|
|
2911
|
+
return stages.map((row) => `${display(row?.label || row?.key)} ${display(row?.count, 0)}`).join(" | ");
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
function mixRow(row) {
|
|
2915
|
+
return [
|
|
2916
|
+
display(row?.label || row?.key),
|
|
2917
|
+
display(row?.count, 0),
|
|
2918
|
+
percentageLabel(row?.percentage)
|
|
2919
|
+
];
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2048
2922
|
function analyticsWindowLabel(payload) {
|
|
2049
2923
|
const window = payload?.window || {};
|
|
2050
2924
|
const key = display(window.key, "24h");
|
|
@@ -2053,18 +2927,67 @@ function analyticsWindowLabel(payload) {
|
|
|
2053
2927
|
return `${key}: ${window.started_at} to ${window.ended_at}`;
|
|
2054
2928
|
}
|
|
2055
2929
|
|
|
2930
|
+
function analyticsActivityLabel(payload) {
|
|
2931
|
+
const range = payload?.date_range;
|
|
2932
|
+
if (range?.start_date && range?.end_date) {
|
|
2933
|
+
return `${range.start_date} to ${range.end_date}`;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
return analyticsWindowLabel(payload);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2056
2939
|
function percentageLabel(value) {
|
|
2057
2940
|
return value === undefined || value === null || value === "" ? "n/a" : `${value}%`;
|
|
2058
2941
|
}
|
|
2059
2942
|
|
|
2060
|
-
function
|
|
2943
|
+
function humanize(value) {
|
|
2944
|
+
const words = String(value || "").trim().replaceAll("-", "_").split("_").filter(Boolean);
|
|
2945
|
+
if (words.length === 0) return "-";
|
|
2946
|
+
|
|
2947
|
+
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function writeOperatorRows(context, rows) {
|
|
2951
|
+
writeAlignedTable(context, ["MOVE ID", "WORK TYPE", "SUBJECT", "MOTION", "NEXT ACTION"], rows.map(operatorTableRow));
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
function operatorTableRow(row) {
|
|
2061
2955
|
return [
|
|
2062
2956
|
display(row?.id),
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2957
|
+
operatorWorkTypeLabel(row),
|
|
2958
|
+
operatorSubjectLabel(row),
|
|
2959
|
+
operatorMotionLabel(row),
|
|
2066
2960
|
display(nextActionLabel(row))
|
|
2067
|
-
]
|
|
2961
|
+
];
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
function operatorWorkTypeLabel(row) {
|
|
2965
|
+
return humanize(row?.opportunity_kind || row?.source_kind);
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function operatorSubjectLabel(row) {
|
|
2969
|
+
return display(
|
|
2970
|
+
row?.prospect?.display_name ||
|
|
2971
|
+
row?.prospect?.name ||
|
|
2972
|
+
row?.profile?.display_name ||
|
|
2973
|
+
row?.profile?.username ||
|
|
2974
|
+
postLabel(row?.post) ||
|
|
2975
|
+
row?.display_name ||
|
|
2976
|
+
row?.name
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
function operatorMotionLabel(row) {
|
|
2981
|
+
return display(row?.motion?.name || row?.motion?.display_name || row?.motion?.prefix_id || row?.motion?.id);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
function postLabel(post) {
|
|
2985
|
+
if (!post) return "";
|
|
2986
|
+
|
|
2987
|
+
const body = String(post.body || "").trim().replace(/\s+/g, " ");
|
|
2988
|
+
if (body) return body.length > 48 ? `${body.slice(0, 45)}...` : body;
|
|
2989
|
+
|
|
2990
|
+
return post.url || (post.id ? `Post ${post.id}` : "");
|
|
2068
2991
|
}
|
|
2069
2992
|
|
|
2070
2993
|
function nextActionLabel(source) {
|
|
@@ -2329,65 +3252,77 @@ const HELP_TOPICS = new Map([
|
|
|
2329
3252
|
"Usage:",
|
|
2330
3253
|
" audienti <command> [options]",
|
|
2331
3254
|
"",
|
|
2332
|
-
"Start
|
|
2333
|
-
" audienti
|
|
2334
|
-
"",
|
|
2335
|
-
"
|
|
2336
|
-
" audienti
|
|
2337
|
-
"
|
|
2338
|
-
"
|
|
2339
|
-
"
|
|
2340
|
-
"
|
|
2341
|
-
"
|
|
2342
|
-
"
|
|
2343
|
-
"
|
|
2344
|
-
"
|
|
2345
|
-
"
|
|
2346
|
-
"
|
|
2347
|
-
"
|
|
2348
|
-
"
|
|
2349
|
-
"
|
|
2350
|
-
"
|
|
2351
|
-
"
|
|
2352
|
-
"
|
|
2353
|
-
"
|
|
2354
|
-
"
|
|
2355
|
-
"
|
|
2356
|
-
"
|
|
2357
|
-
"
|
|
2358
|
-
"
|
|
2359
|
-
"
|
|
2360
|
-
"
|
|
2361
|
-
"
|
|
2362
|
-
"
|
|
2363
|
-
"
|
|
2364
|
-
"
|
|
2365
|
-
"
|
|
2366
|
-
"
|
|
2367
|
-
"
|
|
2368
|
-
"
|
|
2369
|
-
"
|
|
2370
|
-
"
|
|
2371
|
-
"
|
|
2372
|
-
"
|
|
2373
|
-
"
|
|
2374
|
-
"
|
|
2375
|
-
"
|
|
2376
|
-
"
|
|
2377
|
-
"
|
|
2378
|
-
"
|
|
2379
|
-
"
|
|
2380
|
-
"
|
|
2381
|
-
"
|
|
2382
|
-
"",
|
|
2383
|
-
"
|
|
2384
|
-
"
|
|
3255
|
+
"Start:",
|
|
3256
|
+
" audienti auth token <token> Save an API token",
|
|
3257
|
+
" audienti accounts list See accounts available to this token",
|
|
3258
|
+
" audienti accounts select <acct_id> Use one account by default",
|
|
3259
|
+
" audienti help agent-workflows Common agent/operator paths",
|
|
3260
|
+
"",
|
|
3261
|
+
"Work areas:",
|
|
3262
|
+
" Setup & identity",
|
|
3263
|
+
" audienti auth status",
|
|
3264
|
+
" audienti config list",
|
|
3265
|
+
" audienti users list",
|
|
3266
|
+
"",
|
|
3267
|
+
" Motions / plays",
|
|
3268
|
+
" audienti motions list",
|
|
3269
|
+
" audienti motions show <motn_id>",
|
|
3270
|
+
" audienti motions analytics <motn_id>",
|
|
3271
|
+
" audienti motions prospects <motn_id>",
|
|
3272
|
+
" audienti motions create --payload <file.json>",
|
|
3273
|
+
" Tip: `plays` is accepted anywhere `motions` is accepted.",
|
|
3274
|
+
"",
|
|
3275
|
+
" Prospects",
|
|
3276
|
+
" audienti prospects list [filters]",
|
|
3277
|
+
" audienti prospects show <prsp_id>",
|
|
3278
|
+
" audienti prospects timeline <prsp_id>",
|
|
3279
|
+
" audienti prospects import <linkedin_url> [--motion <motn_id>]",
|
|
3280
|
+
" audienti prospects add-note <prsp_id> --message <text>",
|
|
3281
|
+
" audienti prospects add-profile <prsp_id> --url <profile_url|email|phone>",
|
|
3282
|
+
"",
|
|
3283
|
+
" Lists & targeting inputs",
|
|
3284
|
+
" audienti lists list",
|
|
3285
|
+
" audienti lists prospects <list_id>",
|
|
3286
|
+
" audienti offers list",
|
|
3287
|
+
" audienti icps list",
|
|
3288
|
+
" audienti companies search --query <text>",
|
|
3289
|
+
"",
|
|
3290
|
+
" Writer",
|
|
3291
|
+
" audienti writer test-run <prsp_id>",
|
|
3292
|
+
" audienti prospects write <prsp_id> --type <surface_key>",
|
|
3293
|
+
" audienti prospects sequence-export <prsp_id>",
|
|
3294
|
+
"",
|
|
3295
|
+
" Operator queue",
|
|
3296
|
+
" audienti operator next --plan",
|
|
3297
|
+
" audienti operator next --done --note <text>",
|
|
3298
|
+
" audienti operator queue",
|
|
3299
|
+
"",
|
|
3300
|
+
" Analytics",
|
|
3301
|
+
" audienti analytics prospects --window 24h",
|
|
3302
|
+
" audienti analytics prospects cohort-analysis --weeks 4 --motion <motn_id>",
|
|
3303
|
+
" audienti analytics users --user me --window 30d",
|
|
3304
|
+
" audienti analytics visibility --window 24h --user me",
|
|
3305
|
+
" audienti analytics content --window week",
|
|
3306
|
+
"",
|
|
3307
|
+
" Utilities",
|
|
3308
|
+
" audienti tools get email --url <linkedin_url>",
|
|
3309
|
+
" audienti tools get phone --url <linkedin_url>",
|
|
3310
|
+
"",
|
|
3311
|
+
"Common flows:",
|
|
3312
|
+
" Work the next move: audienti operator next --plan",
|
|
3313
|
+
" Inspect a prospect: audienti prospects show <prsp_id> --json",
|
|
3314
|
+
" Preview a campaign: audienti writer test-run <prsp_id>",
|
|
3315
|
+
" Analyze one motion: audienti motions analytics <motn_id>",
|
|
3316
|
+
" Audit your work: audienti analytics users --user me --window 30d",
|
|
2385
3317
|
"",
|
|
2386
3318
|
"Global options:",
|
|
2387
3319
|
" --account <acct_id> Use an account for one command without saving it",
|
|
2388
3320
|
" --help, -h Show help",
|
|
2389
3321
|
"",
|
|
2390
|
-
"
|
|
3322
|
+
"More help:",
|
|
3323
|
+
" audienti <area> help Example: audienti prospects help",
|
|
3324
|
+
" audienti <area> <command> help Example: audienti analytics prospects help",
|
|
3325
|
+
" Use --json when another program or agent will consume the output."
|
|
2391
3326
|
].join("\n")],
|
|
2392
3327
|
|
|
2393
3328
|
["auth", [
|
|
@@ -2903,6 +3838,7 @@ const HELP_TOPICS = new Map([
|
|
|
2903
3838
|
" audienti motions list [--json]",
|
|
2904
3839
|
" audienti motions show <motn_id> [--json]",
|
|
2905
3840
|
" audienti motions status <motn_id> [--json]",
|
|
3841
|
+
" audienti motions analytics <motn_id> [--window 30d] [--json]",
|
|
2906
3842
|
" audienti motions prospects <motn_id> [--json]",
|
|
2907
3843
|
" audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
|
|
2908
3844
|
" audienti motions create --payload <file.json> [--json]",
|
|
@@ -2971,6 +3907,27 @@ const HELP_TOPICS = new Map([
|
|
|
2971
3907
|
" GET /api/v1/accounts/:account_id/motions/:id/status.json"
|
|
2972
3908
|
].join("\n")],
|
|
2973
3909
|
|
|
3910
|
+
["motions analytics", [
|
|
3911
|
+
"Usage:",
|
|
3912
|
+
` ${MOTIONS_ANALYTICS_USAGE.slice("Usage: ".length)}`,
|
|
3913
|
+
"",
|
|
3914
|
+
"Status: implemented",
|
|
3915
|
+
"",
|
|
3916
|
+
"Purpose:",
|
|
3917
|
+
" Show whether one motion is producing prospect output by day.",
|
|
3918
|
+
"",
|
|
3919
|
+
"Options:",
|
|
3920
|
+
" --window <w> AccountProspect.created_at window to inspect. Defaults to 30d. Maximum 90d.",
|
|
3921
|
+
"",
|
|
3922
|
+
"Output shape:",
|
|
3923
|
+
" motion: selected motion/play, including created_at",
|
|
3924
|
+
" prospects_added_count: prospects produced inside the window",
|
|
3925
|
+
" prospects_by_day[]: date, count, active/inactive counts, and current queue_stages for that produced-day cohort",
|
|
3926
|
+
"",
|
|
3927
|
+
"API:",
|
|
3928
|
+
" GET /api/v1/accounts/:account_id/analytics/prospects.json?motion_id=:motion_id"
|
|
3929
|
+
].join("\n")],
|
|
3930
|
+
|
|
2974
3931
|
["motions prospects", [
|
|
2975
3932
|
"Usage:",
|
|
2976
3933
|
" audienti motions prospects <motn_id> [--json] [--account <acct_id>]",
|
|
@@ -3358,9 +4315,54 @@ const HELP_TOPICS = new Map([
|
|
|
3358
4315
|
" POST /api/v1/accounts/:account_id/prospects/:id/sequence_preview.json"
|
|
3359
4316
|
].join("\n")],
|
|
3360
4317
|
|
|
4318
|
+
["writer", [
|
|
4319
|
+
"Usage:",
|
|
4320
|
+
` ${WRITER_TEST_RUN_USAGE.slice("Usage: ".length)}`,
|
|
4321
|
+
"",
|
|
4322
|
+
"Status: implemented",
|
|
4323
|
+
"",
|
|
4324
|
+
"Purpose:",
|
|
4325
|
+
" Run a writer campaign test for one prospect: resolve their current context, simulate the full no-reply path, and draft each message step.",
|
|
4326
|
+
"",
|
|
4327
|
+
"Commands:",
|
|
4328
|
+
" audienti writer test-run <prsp_id>",
|
|
4329
|
+
"",
|
|
4330
|
+
"Alias:",
|
|
4331
|
+
" audienti writers test-run <prsp_id>"
|
|
4332
|
+
].join("\n")],
|
|
4333
|
+
|
|
4334
|
+
["writer test-run", [
|
|
4335
|
+
"Usage:",
|
|
4336
|
+
` ${WRITER_TEST_RUN_USAGE.slice("Usage: ".length)}`,
|
|
4337
|
+
"",
|
|
4338
|
+
"Status: implemented",
|
|
4339
|
+
"",
|
|
4340
|
+
"Purpose:",
|
|
4341
|
+
" Run the prospect-scoped writer test run for a single prospect.",
|
|
4342
|
+
"",
|
|
4343
|
+
"Behavior:",
|
|
4344
|
+
" Uses the same Prospects::SequencePreview simulator as the app, resolves the prospect's motion/agent/ICP/offer context, runs no-reply branches, and returns the full campaign path with actions, waits, channel changes, drafted message bodies, and terminal disposition.",
|
|
4345
|
+
"",
|
|
4346
|
+
"Options:",
|
|
4347
|
+
" --mode <mode> report drafts every message, plan skips drafting, step drafts one selected step",
|
|
4348
|
+
" --branch <branch> Optional branch filter: both | no-accept | accepted",
|
|
4349
|
+
" --step <step_key|row_number> Required with --mode step. Row numbers come from the # column.",
|
|
4350
|
+
" --no-cache Plan/step modes ignore locally cached simulator drafts",
|
|
4351
|
+
" --clear-cache Plan/step modes clear locally cached simulator drafts before running",
|
|
4352
|
+
"",
|
|
4353
|
+
"Output shape:",
|
|
4354
|
+
" branches[].key: no_accept | accepted",
|
|
4355
|
+
" branches[].steps[]: ordered wait/action/message/terminal steps for that simulated path",
|
|
4356
|
+
" branches[].steps[].body: draft copy for message steps when the writer can generate one",
|
|
4357
|
+
" branches[].summary: channel sequence, touch counts, duration, terminal disposition",
|
|
4358
|
+
"",
|
|
4359
|
+
"API:",
|
|
4360
|
+
" POST /api/v1/accounts/:account_id/prospects/:id/sequence_export.json"
|
|
4361
|
+
].join("\n")],
|
|
4362
|
+
|
|
3361
4363
|
["prospects sequence-export", [
|
|
3362
4364
|
"Usage:",
|
|
3363
|
-
" audienti prospects sequence-export <prsp_id> [--json|--csv] [--branch <both|no-accept|accepted>] [--angle-index <n>] [--account <acct_id>]",
|
|
4365
|
+
" audienti prospects sequence-export <prsp_id> [--json|--csv] [--branch <both|no-accept|accepted>] [--draft-mode <all|plan|target>] [--target-step <step_key|row_number>] [--angle-index <n>] [--account <acct_id>]",
|
|
3364
4366
|
"",
|
|
3365
4367
|
"Status: implemented",
|
|
3366
4368
|
"",
|
|
@@ -3372,6 +4374,11 @@ const HELP_TOPICS = new Map([
|
|
|
3372
4374
|
" no-accept: no reply and no accepted connection request",
|
|
3373
4375
|
" accepted: connection request accepted, then no reply otherwise",
|
|
3374
4376
|
"",
|
|
4377
|
+
"Draft modes:",
|
|
4378
|
+
" all: default. Draft every message step",
|
|
4379
|
+
" plan: build the timeline without drafting message bodies",
|
|
4380
|
+
" target: draft only --target-step and return the branch prefix through that step. Row numbers use rows[].step_number.",
|
|
4381
|
+
"",
|
|
3375
4382
|
"Output shape:",
|
|
3376
4383
|
" rows[].prospect_id: prsp_",
|
|
3377
4384
|
" rows[].branch: no_accept | accepted",
|
|
@@ -3590,7 +4597,9 @@ const HELP_TOPICS = new Map([
|
|
|
3590
4597
|
|
|
3591
4598
|
["analytics", [
|
|
3592
4599
|
"Usage:",
|
|
3593
|
-
" audienti analytics prospects [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
4600
|
+
" audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--user <account_user_id|email|name|me>] [--json]",
|
|
4601
|
+
" audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--user <account_user_id|email|name|me>] [--json]",
|
|
4602
|
+
" audienti analytics users [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--platform <linkedin|email|gmail>] [--json]",
|
|
3594
4603
|
" audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
3595
4604
|
" audienti analytics visops [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
3596
4605
|
" audienti analytics content [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
@@ -3599,23 +4608,56 @@ const HELP_TOPICS = new Map([
|
|
|
3599
4608
|
"",
|
|
3600
4609
|
"Window:",
|
|
3601
4610
|
" --window <24h|7d|1w|day|week>",
|
|
3602
|
-
" --
|
|
4611
|
+
" --start <YYYY-MM-DD> --end <YYYY-MM-DD> For user analytics, select the events.created_at activity range instead of --window.",
|
|
4612
|
+
" --cohort-start <YYYY-MM-DD> --cohort-end <YYYY-MM-DD> Select the AccountProspect.created_at cohort while --window or --start/--end selects the activity period.",
|
|
4613
|
+
" --motion <motn_id> For prospect and user analytics, filter AccountProspect.motion_id to one motion/play.",
|
|
4614
|
+
" --provenance <source> Optional lower-level AccountProspect.intake_source filter.",
|
|
4615
|
+
" --platform <linkedin|email|gmail> For user analytics, filter events.platform. --channel is accepted as an alias.",
|
|
4616
|
+
" cohort-analysis loops over recent weekly AccountProspect.created_at cohorts and compares their current stages.",
|
|
4617
|
+
" --user <account_user_id|email|name|me> Narrow analytics to one account user. For prospect analytics, this means prospects assigned to that account user. Email/name partials are accepted when they match exactly one account user.",
|
|
3603
4618
|
"",
|
|
3604
4619
|
"Output:",
|
|
3605
|
-
" Account-scoped analytics for prospects, visibility engagement, and ContentOps publishing."
|
|
4620
|
+
" Account-scoped analytics for prospects, users, visibility engagement, and ContentOps publishing."
|
|
3606
4621
|
].join("\n")],
|
|
3607
4622
|
|
|
3608
4623
|
["analytics prospects", [
|
|
3609
4624
|
"Usage:",
|
|
3610
|
-
" audienti analytics prospects [--window 24h] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]",
|
|
4625
|
+
" audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]",
|
|
4626
|
+
" audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]",
|
|
3611
4627
|
"",
|
|
3612
4628
|
"Status: implemented",
|
|
3613
4629
|
"",
|
|
3614
4630
|
"Output shape:",
|
|
3615
|
-
"
|
|
4631
|
+
" window: activity/event period for actions",
|
|
4632
|
+
" cohort: selected AccountProspect.created_at cohort when cohort dates are provided",
|
|
4633
|
+
" motion: selected motion/play when --motion is provided",
|
|
4634
|
+
" provenance: selected AccountProspect.intake_source when --provenance is provided",
|
|
4635
|
+
" prospects_added_count: account prospects added in the window, or cohort size when cohort dates are provided",
|
|
4636
|
+
" cohort_prospects_count: selected AccountProspect.created_at cohort size when cohort dates are provided",
|
|
3616
4637
|
" account_user: selected account user when --user is provided, otherwise null",
|
|
3617
|
-
"
|
|
3618
|
-
"
|
|
4638
|
+
" --user filters AccountProspect.assigned_to_account_user_id, so `--user me` reports prospects assigned to you",
|
|
4639
|
+
" actions: outbound action totals in the window, narrowed to cohort prospects when cohort dates are provided",
|
|
4640
|
+
" queue_stages[]: current account prospect stage counts, narrowed to the selected cohort when cohort dates are provided",
|
|
4641
|
+
"",
|
|
4642
|
+
"API:",
|
|
4643
|
+
" GET /api/v1/accounts/:account_id/analytics/prospects.json"
|
|
4644
|
+
].join("\n")],
|
|
4645
|
+
|
|
4646
|
+
["analytics prospects cohort-analysis", [
|
|
4647
|
+
"Usage:",
|
|
4648
|
+
` ${ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE.slice("Usage: ".length)}`,
|
|
4649
|
+
"",
|
|
4650
|
+
"Status: implemented",
|
|
4651
|
+
"",
|
|
4652
|
+
"Behavior:",
|
|
4653
|
+
" Calls the prospect analytics endpoint once per weekly AccountProspect.created_at cohort, then renders current pipeline-stage counts side by side so older cohorts can be compared against newer cohorts.",
|
|
4654
|
+
"",
|
|
4655
|
+
"Options:",
|
|
4656
|
+
" --weeks <n> Number of calendar-week cohorts to inspect. Defaults to 4. Maximum 26.",
|
|
4657
|
+
" --window <w> Activity window passed through to each analytics call. Defaults to 24h.",
|
|
4658
|
+
" --motion <motn_id> Optional motion/play filter.",
|
|
4659
|
+
" --provenance <source> Optional AccountProspect.intake_source filter.",
|
|
4660
|
+
" --user <id> Optional account-user filter.",
|
|
3619
4661
|
"",
|
|
3620
4662
|
"API:",
|
|
3621
4663
|
" GET /api/v1/accounts/:account_id/analytics/prospects.json"
|
|
@@ -3628,6 +4670,43 @@ const HELP_TOPICS = new Map([
|
|
|
3628
4670
|
"Alias for `audienti analytics prospects`."
|
|
3629
4671
|
].join("\n")],
|
|
3630
4672
|
|
|
4673
|
+
["analytics users", [
|
|
4674
|
+
"Usage:",
|
|
4675
|
+
` ${ANALYTICS_USERS_USAGE.slice("Usage: ".length)}`,
|
|
4676
|
+
"",
|
|
4677
|
+
"Status: implemented",
|
|
4678
|
+
"",
|
|
4679
|
+
"Purpose:",
|
|
4680
|
+
" Audit one account user's outbound action history with the same actor semantics used by the Operations user analytics page.",
|
|
4681
|
+
"",
|
|
4682
|
+
"Options:",
|
|
4683
|
+
" --user <account_user_id|email|name|me> Defaults to me.",
|
|
4684
|
+
" --window <w> Activity window. Defaults to 30d when --start/--end are not provided.",
|
|
4685
|
+
" --start <YYYY-MM-DD> --end <YYYY-MM-DD> Explicit events.created_at activity range.",
|
|
4686
|
+
" --cohort-start <YYYY-MM-DD> --cohort-end <YYYY-MM-DD> Optional AccountProspect.created_at cohort filter.",
|
|
4687
|
+
" --motion <motn_id> Optional motion/play filter.",
|
|
4688
|
+
" --provenance <source> Optional AccountProspect.intake_source filter.",
|
|
4689
|
+
" --platform <linkedin|email|gmail> Optional events.platform filter. `email` includes email and gmail rows; --channel is an alias.",
|
|
4690
|
+
"",
|
|
4691
|
+
"Output shape:",
|
|
4692
|
+
" account_user: selected account user",
|
|
4693
|
+
" summary: performed-by-user totals and performed-by-others comparison",
|
|
4694
|
+
" daily_actions[]: action counts by events.created_at date",
|
|
4695
|
+
" action_mix[]: action type counts and percentages",
|
|
4696
|
+
" platform: selected platform/channel filter when --platform or --channel is provided",
|
|
4697
|
+
" platform_mix[]: platform counts and percentages",
|
|
4698
|
+
"",
|
|
4699
|
+
"API:",
|
|
4700
|
+
" GET /api/v1/accounts/:account_id/analytics/users.json"
|
|
4701
|
+
].join("\n")],
|
|
4702
|
+
|
|
4703
|
+
["analytics user", [
|
|
4704
|
+
"Usage:",
|
|
4705
|
+
" audienti analytics user [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--json] [--account <acct_id>]",
|
|
4706
|
+
"",
|
|
4707
|
+
"Alias for `audienti analytics users`."
|
|
4708
|
+
].join("\n")],
|
|
4709
|
+
|
|
3631
4710
|
["analytics visibility", [
|
|
3632
4711
|
"Usage:",
|
|
3633
4712
|
" audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]",
|
|
@@ -3703,6 +4782,7 @@ const HELP_TOPICS = new Map([
|
|
|
3703
4782
|
" audienti prospects report-bad-profile <prsp_id> <prof_id>",
|
|
3704
4783
|
" audienti prospects add-note <prsp_id> --type steer --message \"Meeting will not happen\" --engagement-type action.meeting.canceled",
|
|
3705
4784
|
" audienti prospects sequence-preview <prsp_id>",
|
|
4785
|
+
" audienti writer test-run <prsp_id>",
|
|
3706
4786
|
" audienti prospects sequence-export <prsp_id> --csv",
|
|
3707
4787
|
"",
|
|
3708
4788
|
"5. Attach existing prospects without re-importing",
|
|
@@ -3717,6 +4797,7 @@ const HELP_TOPICS = new Map([
|
|
|
3717
4797
|
"",
|
|
3718
4798
|
"7. Inspect account analytics",
|
|
3719
4799
|
" audienti analytics prospects --window 24h",
|
|
4800
|
+
" audienti analytics users --user me --window 30d",
|
|
3720
4801
|
" audienti analytics visibility --window 24h --user me",
|
|
3721
4802
|
" audienti analytics content --window week",
|
|
3722
4803
|
"",
|