@audienti/cli 0.1.5 → 0.1.6
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 +16 -0
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/api-client.js +4 -0
- package/src/cli.js +1156 -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,160 @@ 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))
|
|
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
|
+
generated_at: entry.generated_at,
|
|
1129
|
+
writer_engine: entry.writer_engine,
|
|
1130
|
+
target: entry.target,
|
|
1131
|
+
metadata: entry.metadata
|
|
1132
|
+
}));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function writerRequestedBranchKeys(branchFilter) {
|
|
1136
|
+
const values = String(branchFilter || "both").split(",").map((value) => value.trim()).filter(Boolean);
|
|
1137
|
+
if (values.length === 0 || values.includes("both")) return ["no_accept", "accepted"];
|
|
1138
|
+
|
|
1139
|
+
return values.map((value) => {
|
|
1140
|
+
const normalized = value.replaceAll("-", "_");
|
|
1141
|
+
if (["default", "no_accept", "not_connected"].includes(normalized)) return "no_accept";
|
|
1142
|
+
if (normalized === "accepted") return "accepted";
|
|
1143
|
+
return normalized;
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function writerCacheMeta(cache, cachedDrafts) {
|
|
1148
|
+
return {
|
|
1149
|
+
enabled: true,
|
|
1150
|
+
path: cache.path,
|
|
1151
|
+
entry_count: Object.keys(cache.entries || {}).length,
|
|
1152
|
+
sent_draft_count: cachedDrafts.length
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async function persistWriterDraftsFromPayload(context, { cache, payload }) {
|
|
1157
|
+
const entries = { ...(cache.entries || {}) };
|
|
1158
|
+
let changed = false;
|
|
1159
|
+
|
|
1160
|
+
for (const branch of Array.isArray(payload?.branches) ? payload.branches : []) {
|
|
1161
|
+
const branchKey = String(branch?.key || "").trim();
|
|
1162
|
+
if (!branchKey) continue;
|
|
1163
|
+
|
|
1164
|
+
for (const step of Array.isArray(branch?.steps) ? branch.steps : []) {
|
|
1165
|
+
const entry = writerCacheEntryFromStep(step, { branchKey, generatedAt: branch.generated_at || payload?.generated_at });
|
|
1166
|
+
if (!entry) continue;
|
|
1167
|
+
|
|
1168
|
+
entries[writerCacheEntryKey(entry)] = entry;
|
|
1169
|
+
changed = true;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (!changed) return;
|
|
1174
|
+
|
|
1175
|
+
const nextCache = {
|
|
1176
|
+
version: WRITER_TEST_RUN_CACHE_VERSION,
|
|
1177
|
+
account_id: cache.account_id,
|
|
1178
|
+
prospect_id: cache.prospect_id,
|
|
1179
|
+
updated_at: new Date().toISOString(),
|
|
1180
|
+
entries
|
|
1181
|
+
};
|
|
1182
|
+
await mkdir(dirname(cache.path), { recursive: true });
|
|
1183
|
+
await writeFile(cache.path, `${JSON.stringify(nextCache, null, 2)}\n`, "utf8");
|
|
1184
|
+
cache.entries = entries;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function writerCacheEntryFromStep(step, { branchKey, generatedAt }) {
|
|
1188
|
+
if (step?.kind !== "message") return null;
|
|
1189
|
+
if (!step.key) return null;
|
|
1190
|
+
if (!(step.body || step.text || step.subject)) return null;
|
|
1191
|
+
if (step.status === "planned" || step.status === "unavailable" || step.status === "error") return null;
|
|
1192
|
+
|
|
1193
|
+
return compactObject({
|
|
1194
|
+
branch: branchKey,
|
|
1195
|
+
key: step.key,
|
|
1196
|
+
stage: step.stage,
|
|
1197
|
+
channel: step.channel,
|
|
1198
|
+
platform: step.platform,
|
|
1199
|
+
message_mode: step.message_mode,
|
|
1200
|
+
subject: step.subject,
|
|
1201
|
+
body: step.body,
|
|
1202
|
+
text: step.text,
|
|
1203
|
+
status: step.status,
|
|
1204
|
+
generated_at: step.generated_at || generatedAt || new Date().toISOString(),
|
|
1205
|
+
writer_engine: step?.metadata?.writer_engine,
|
|
1206
|
+
target: step.target,
|
|
1207
|
+
metadata: step.metadata
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function writerCacheEntryKey(entry) {
|
|
1212
|
+
return `${entry.branch}:${entry.key}`;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
960
1215
|
async function prospectsImport(args, context, { accountOverride } = {}) {
|
|
961
1216
|
const { values, positionals } = parseCommandArgs(args, {
|
|
962
1217
|
...jsonOptions(),
|
|
@@ -1102,8 +1357,12 @@ async function operatorOutcome(args, context, { accountOverride } = {}) {
|
|
|
1102
1357
|
}
|
|
1103
1358
|
|
|
1104
1359
|
async function analyticsProspects(args, context, { accountOverride } = {}) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1360
|
+
if (args[0] === "cohort-analysis") {
|
|
1361
|
+
return analyticsProspectsCohortAnalysis(args.slice(1), context, { accountOverride });
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const { values, positionals } = parseCommandArgs(args, analyticsProspectsOptions());
|
|
1365
|
+
if (positionals.length > 0) throw new CommandError(ANALYTICS_PROSPECTS_USAGE);
|
|
1107
1366
|
|
|
1108
1367
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
1109
1368
|
const payload = await client.analyticsProspects(accountId, analyticsQuery(values));
|
|
@@ -1112,6 +1371,61 @@ async function analyticsProspects(args, context, { accountOverride } = {}) {
|
|
|
1112
1371
|
renderAnalyticsProspects(payload, context);
|
|
1113
1372
|
}
|
|
1114
1373
|
|
|
1374
|
+
async function analyticsProspectsCohortAnalysis(args, context, { accountOverride } = {}) {
|
|
1375
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
1376
|
+
...jsonOptions(),
|
|
1377
|
+
weeks: { type: "string" },
|
|
1378
|
+
window: { type: "string" },
|
|
1379
|
+
motion: { type: "string" },
|
|
1380
|
+
provenance: { type: "string" },
|
|
1381
|
+
user: { type: "string" }
|
|
1382
|
+
});
|
|
1383
|
+
if (positionals.length > 0) throw new CommandError(ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE);
|
|
1384
|
+
|
|
1385
|
+
const weeks = normalizedCohortAnalysisWeeks(values.weeks);
|
|
1386
|
+
const cohorts = weeklyCohorts({ weeks, now: currentDate(context) });
|
|
1387
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
1388
|
+
const rows = [];
|
|
1389
|
+
|
|
1390
|
+
for (const cohort of cohorts) {
|
|
1391
|
+
const payload = await client.analyticsProspects(accountId, compactObject({
|
|
1392
|
+
window: values.window,
|
|
1393
|
+
account_user_id: values.user,
|
|
1394
|
+
motion_id: values.motion,
|
|
1395
|
+
provenance: values.provenance,
|
|
1396
|
+
cohort_start: cohort.start_date,
|
|
1397
|
+
cohort_end: cohort.end_date
|
|
1398
|
+
}));
|
|
1399
|
+
rows.push(cohortAnalysisRow(payload, cohort));
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const payload = {
|
|
1403
|
+
kind: "prospect_cohort_analysis",
|
|
1404
|
+
weeks,
|
|
1405
|
+
window: values.window || "24h",
|
|
1406
|
+
motion: rows.find((row) => row.motion)?.motion || motionPayload(values.motion),
|
|
1407
|
+
provenance: rows.find((row) => row.provenance)?.provenance || provenancePayload(values.provenance),
|
|
1408
|
+
account_user: rows.find((row) => row.account_user)?.account_user || null,
|
|
1409
|
+
cohorts: rows
|
|
1410
|
+
};
|
|
1411
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
1412
|
+
|
|
1413
|
+
renderAnalyticsProspectCohortAnalysis(payload, context);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
async function analyticsUsers(args, context, { accountOverride } = {}) {
|
|
1417
|
+
const { values, positionals } = parseCommandArgs(args, analyticsUsersOptions());
|
|
1418
|
+
if (positionals.length > 0) throw new CommandError(ANALYTICS_USERS_USAGE);
|
|
1419
|
+
validateDatePair(values.start, values.end, "--start", "--end");
|
|
1420
|
+
validateDatePair(values["cohort-start"], values["cohort-end"], "--cohort-start", "--cohort-end");
|
|
1421
|
+
|
|
1422
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
1423
|
+
const payload = await client.analyticsUsers(accountId, analyticsUsersQuery(values));
|
|
1424
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
1425
|
+
|
|
1426
|
+
renderAnalyticsUsers(payload, context);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1115
1429
|
async function analyticsVisibility(args, context, { accountOverride } = {}) {
|
|
1116
1430
|
const { values, positionals } = parseCommandArgs(args, analyticsOptions());
|
|
1117
1431
|
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 +1574,161 @@ function analyticsOptions() {
|
|
|
1260
1574
|
};
|
|
1261
1575
|
}
|
|
1262
1576
|
|
|
1577
|
+
function analyticsProspectsOptions() {
|
|
1578
|
+
return {
|
|
1579
|
+
...analyticsOptions(),
|
|
1580
|
+
"cohort-start": { type: "string" },
|
|
1581
|
+
"cohort-end": { type: "string" },
|
|
1582
|
+
motion: { type: "string" },
|
|
1583
|
+
provenance: { type: "string" }
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function analyticsUsersOptions() {
|
|
1588
|
+
return {
|
|
1589
|
+
...jsonOptions(),
|
|
1590
|
+
user: { type: "string" },
|
|
1591
|
+
window: { type: "string" },
|
|
1592
|
+
start: { type: "string" },
|
|
1593
|
+
end: { type: "string" },
|
|
1594
|
+
"cohort-start": { type: "string" },
|
|
1595
|
+
"cohort-end": { type: "string" },
|
|
1596
|
+
motion: { type: "string" },
|
|
1597
|
+
provenance: { type: "string" },
|
|
1598
|
+
platform: { type: "string" },
|
|
1599
|
+
channel: { type: "string" }
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1263
1603
|
function analyticsQuery(values) {
|
|
1264
1604
|
return compactObject({
|
|
1265
1605
|
window: values.window,
|
|
1606
|
+
cohort_start: values["cohort-start"],
|
|
1607
|
+
cohort_end: values["cohort-end"],
|
|
1608
|
+
motion_id: values.motion,
|
|
1609
|
+
provenance: values.provenance,
|
|
1266
1610
|
account_user_id: values.user
|
|
1267
1611
|
});
|
|
1268
1612
|
}
|
|
1269
1613
|
|
|
1614
|
+
function analyticsUsersQuery(values) {
|
|
1615
|
+
const hasDateRange = Boolean(values.start || values.end);
|
|
1616
|
+
return compactObject({
|
|
1617
|
+
account_user_id: values.user || "me",
|
|
1618
|
+
window: hasDateRange ? undefined : (values.window || "30d"),
|
|
1619
|
+
start_date: values.start,
|
|
1620
|
+
end_date: values.end,
|
|
1621
|
+
cohort_start: values["cohort-start"],
|
|
1622
|
+
cohort_end: values["cohort-end"],
|
|
1623
|
+
motion_id: values.motion,
|
|
1624
|
+
provenance: values.provenance,
|
|
1625
|
+
platform: values.platform || values.channel
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function validateDatePair(start, end, startFlag, endFlag) {
|
|
1630
|
+
if ((start && !end) || (!start && end)) {
|
|
1631
|
+
throw new CommandError(`${startFlag} and ${endFlag} must be provided together.`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function normalizedCohortAnalysisWeeks(rawValue) {
|
|
1636
|
+
const weeks = Number.parseInt(rawValue || "4", 10);
|
|
1637
|
+
if (!Number.isInteger(weeks) || weeks <= 0) {
|
|
1638
|
+
throw new CommandError("--weeks must be a positive integer.");
|
|
1639
|
+
}
|
|
1640
|
+
if (weeks > 26) {
|
|
1641
|
+
throw new CommandError("--weeks must be 26 or less.");
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return weeks;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function currentDate(context) {
|
|
1648
|
+
const raw = typeof context.now === "function" ? context.now() : context.now;
|
|
1649
|
+
const date = raw instanceof Date ? raw : new Date(raw || Date.now());
|
|
1650
|
+
if (Number.isNaN(date.getTime())) return utcDateOnly(new Date());
|
|
1651
|
+
|
|
1652
|
+
return utcDateOnly(date);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function weeklyCohorts({ weeks, now }) {
|
|
1656
|
+
const currentWeekStart = startOfUtcWeek(now);
|
|
1657
|
+
const rows = [];
|
|
1658
|
+
|
|
1659
|
+
for (let offset = weeks - 1; offset >= 0; offset -= 1) {
|
|
1660
|
+
const start = addUtcDays(currentWeekStart, offset * -7);
|
|
1661
|
+
const plannedEnd = addUtcDays(start, 6);
|
|
1662
|
+
const end = plannedEnd > now ? now : plannedEnd;
|
|
1663
|
+
rows.push({
|
|
1664
|
+
start_date: isoDate(start),
|
|
1665
|
+
end_date: isoDate(end)
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
return rows;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function startOfUtcWeek(date) {
|
|
1673
|
+
const day = date.getUTCDay();
|
|
1674
|
+
const mondayOffset = (day + 6) % 7;
|
|
1675
|
+
return addUtcDays(date, -mondayOffset);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function utcDateOnly(date) {
|
|
1679
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function addUtcDays(date, days) {
|
|
1683
|
+
const next = new Date(date.getTime());
|
|
1684
|
+
next.setUTCDate(next.getUTCDate() + days);
|
|
1685
|
+
return utcDateOnly(next);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function isoDate(date) {
|
|
1689
|
+
return date.toISOString().slice(0, 10);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function cohortAnalysisRow(payload, fallbackCohort) {
|
|
1693
|
+
const cohort = payload?.cohort || fallbackCohort;
|
|
1694
|
+
const stages = {};
|
|
1695
|
+
const stageLabels = {};
|
|
1696
|
+
for (const row of Array.isArray(payload?.queue_stages) ? payload.queue_stages : []) {
|
|
1697
|
+
const key = String(row?.key || "").trim();
|
|
1698
|
+
if (!key) continue;
|
|
1699
|
+
|
|
1700
|
+
stages[key] = row?.count || 0;
|
|
1701
|
+
stageLabels[key] = row?.label || key;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
return {
|
|
1705
|
+
cohort,
|
|
1706
|
+
label: `${display(cohort.start_date)} to ${display(cohort.end_date)}`,
|
|
1707
|
+
total_count: payload?.cohort_prospects_count ?? payload?.prospects_added_count ?? 0,
|
|
1708
|
+
motion: payload?.motion || null,
|
|
1709
|
+
provenance: payload?.provenance || null,
|
|
1710
|
+
account_user: payload?.account_user || null,
|
|
1711
|
+
stages,
|
|
1712
|
+
stage_labels: stageLabels
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function motionPayload(motionId) {
|
|
1717
|
+
if (!motionId) return null;
|
|
1718
|
+
|
|
1719
|
+
return { prefix_id: motionId, name: motionId };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function provenancePayload(provenance) {
|
|
1723
|
+
if (!provenance) return null;
|
|
1724
|
+
|
|
1725
|
+
return {
|
|
1726
|
+
key: provenance,
|
|
1727
|
+
label: humanize(provenance),
|
|
1728
|
+
field: "account_prospects.intake_source"
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1270
1732
|
function compactObject(object) {
|
|
1271
1733
|
return Object.fromEntries(
|
|
1272
1734
|
Object.entries(object).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== "")
|
|
@@ -1585,10 +2047,16 @@ function renderBulkMutationResult(payload, context, { successLabel, zeroSuccessL
|
|
|
1585
2047
|
function renderMotions(motions, context) {
|
|
1586
2048
|
if (!Array.isArray(motions) || motions.length === 0) return writeLine(context.stdout, "No motions found.");
|
|
1587
2049
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2050
|
+
writeAlignedTable(context, ["MOTION ID", "STATUS", "KIND", "NAME"], motions.map(motionTableRow));
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
function motionTableRow(motion) {
|
|
2054
|
+
return [
|
|
2055
|
+
display(motion?.prefix_id),
|
|
2056
|
+
display(motion?.status),
|
|
2057
|
+
display(motion?.kind),
|
|
2058
|
+
display(motion?.name)
|
|
2059
|
+
];
|
|
1592
2060
|
}
|
|
1593
2061
|
|
|
1594
2062
|
function renderMotion(motion, context) {
|
|
@@ -1773,7 +2241,7 @@ function renderProspectProfileMutation(payload, context, { action }) {
|
|
|
1773
2241
|
if (profile.url) writeLine(context.stdout, `URL: ${profile.url}`);
|
|
1774
2242
|
}
|
|
1775
2243
|
|
|
1776
|
-
function renderProspectSequencePreview(payload, context) {
|
|
2244
|
+
function renderProspectSequencePreview(payload, context, { title = "Sequence preview" } = {}) {
|
|
1777
2245
|
const prospect = payload?.prospect || {};
|
|
1778
2246
|
const report = payload?.report || {};
|
|
1779
2247
|
const preview = report?.last_preview || {};
|
|
@@ -1782,6 +2250,7 @@ function renderProspectSequencePreview(payload, context) {
|
|
|
1782
2250
|
const steps = Array.isArray(report?.steps) ? report.steps : [];
|
|
1783
2251
|
const contextInfo = payload?.context || {};
|
|
1784
2252
|
|
|
2253
|
+
writeLine(context.stdout, title);
|
|
1785
2254
|
writeLine(context.stdout, `Prospect: ${display(prospect.display_name || selected.prospect_name)} (${display(prospect.prefix_id || selected.prospect_id)})`);
|
|
1786
2255
|
if (contextInfo.source) writeLine(context.stdout, `Context: ${contextInfo.source}`);
|
|
1787
2256
|
if (contextInfo.message) writeLine(context.stdout, contextInfo.message);
|
|
@@ -1802,21 +2271,176 @@ function renderProspectSequencePreview(payload, context) {
|
|
|
1802
2271
|
writeLine(context.stdout, "");
|
|
1803
2272
|
writeLine(context.stdout, "Sequence:");
|
|
1804
2273
|
|
|
1805
|
-
steps.forEach((step, index) =>
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
});
|
|
2274
|
+
steps.forEach((step, index) => renderSequenceStep(step, index, context));
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
function renderWriterTestRun(payload, context) {
|
|
2278
|
+
const prospect = payload?.prospect || {};
|
|
2279
|
+
const branches = Array.isArray(payload?.branches) ? payload.branches : [];
|
|
2280
|
+
const draftMode = payload?.meta?.draft_mode || "all";
|
|
2281
|
+
const targetStep = payload?.meta?.target_step;
|
|
2282
|
+
|
|
2283
|
+
writeLine(context.stdout, "Writer campaign simulator");
|
|
2284
|
+
writeLine(context.stdout, `Prospect: ${display(prospect.display_name || prospect.name)} (${display(prospect.prefix_id)})`);
|
|
2285
|
+
if (payload?.context?.source) writeLine(context.stdout, `Context: ${payload.context.source}`);
|
|
2286
|
+
if (payload?.context?.message) writeLine(context.stdout, payload.context.message);
|
|
2287
|
+
if (payload?.context?.motion_name) writeLine(context.stdout, `Motion: ${payload.context.motion_name}`);
|
|
2288
|
+
if (payload?.context?.agent_name) writeLine(context.stdout, `Agent: ${payload.context.agent_name}`);
|
|
2289
|
+
if (payload?.context?.offer_name) writeLine(context.stdout, `Offer: ${payload.context.offer_name}`);
|
|
2290
|
+
writeLine(context.stdout, `Mode: ${display(draftMode)}`);
|
|
2291
|
+
if (targetStep) writeLine(context.stdout, `Target step: ${display(targetStep)}`);
|
|
2292
|
+
if (payload?.meta?.cache?.enabled) {
|
|
2293
|
+
writeLine(context.stdout, `Cache: ${display(payload.meta.cache.path)}`);
|
|
2294
|
+
writeLine(context.stdout, `Cached drafts sent: ${display(payload.meta.cache.sent_draft_count || 0)}`);
|
|
2295
|
+
}
|
|
2296
|
+
writeLine(context.stdout, `Start: ${isoDate(currentDate(context))}`);
|
|
2297
|
+
writeLine(context.stdout, "DATE: step execution date; for WAIT rows, the wait clears on that date.");
|
|
2298
|
+
writeLine(context.stdout, "Scenario: simulate the full path if the prospect does not reply.");
|
|
2299
|
+
if (draftMode === "plan") {
|
|
2300
|
+
writeLine(context.stdout, "Drafts are skipped; this run only plans the path and context.");
|
|
2301
|
+
} else if (draftMode === "target") {
|
|
2302
|
+
writeLine(context.stdout, "Only the target step is drafted; later steps are omitted.");
|
|
2303
|
+
} else {
|
|
2304
|
+
writeLine(context.stdout, "This can take a while because the writer drafts every message step.");
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
if (branches.length === 0) {
|
|
2308
|
+
writeLine(context.stdout, "No simulator branches were generated.");
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
for (const branch of branches) {
|
|
2313
|
+
const steps = Array.isArray(branch.steps) ? branch.steps : [];
|
|
2314
|
+
const summary = branch.summary || {};
|
|
2315
|
+
writeLine(context.stdout, "");
|
|
2316
|
+
writeLine(context.stdout, `${display(branch.label)} (${display(branch.key)})`);
|
|
2317
|
+
if (summary.channel_sequence?.length) writeLine(context.stdout, `Channels: ${summary.channel_sequence.join(" -> ")}`);
|
|
2318
|
+
if (summary.total_duration_days !== undefined) writeLine(context.stdout, `Duration days: ${summary.total_duration_days}`);
|
|
2319
|
+
if (summary.terminal_disposition) writeLine(context.stdout, `Terminal disposition: ${summary.terminal_disposition}`);
|
|
2320
|
+
|
|
2321
|
+
if (steps.length === 0) {
|
|
2322
|
+
writeLine(context.stdout, "No steps.");
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
renderWriterStepTable(steps, context);
|
|
2327
|
+
if (draftMode === "target") renderWriterTargetDraft(branch, context);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function renderWriterStepTable(steps, context) {
|
|
2332
|
+
writeLine(context.stdout, "# DATE DOW TYPE ACTION CH STATUS");
|
|
2333
|
+
writeLine(context.stdout, "-- ---------- --- ---- ------------------------------------------ ------- ---------");
|
|
2334
|
+
steps.forEach((step, index) => renderWriterStepRow(step, index, context));
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
function renderWriterStepRow(step, index, context) {
|
|
2338
|
+
const stepDate = writerStepDate(step, context);
|
|
2339
|
+
const row = [
|
|
2340
|
+
fixedWidth(index + 1, 2, { align: "right" }),
|
|
2341
|
+
fixedWidth(stepDate, 10),
|
|
2342
|
+
fixedWidth(writerStepDow(stepDate), 3),
|
|
2343
|
+
fixedWidth(compactKindLabel(step.kind), 4),
|
|
2344
|
+
fixedWidth(writerStepAction(step), 42),
|
|
2345
|
+
fixedWidth(compactChannelLabel(step.channel), 7),
|
|
2346
|
+
fixedWidth(display(step.status), 9)
|
|
2347
|
+
].join(" ");
|
|
2348
|
+
writeLine(context.stdout, row);
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function renderWriterTargetDraft(branch, context) {
|
|
2352
|
+
const steps = Array.isArray(branch.steps) ? branch.steps : [];
|
|
2353
|
+
const resolvedTargetStep = String(branch.resolved_target_step || "").trim();
|
|
2354
|
+
const draftedStep = steps.find((step) => step?.kind === "message" && step?.key === resolvedTargetStep) ||
|
|
2355
|
+
[...steps].reverse().find((step) => step?.kind === "message" && (step.body || step.text || step.subject));
|
|
2356
|
+
if (!draftedStep) return;
|
|
2357
|
+
|
|
2358
|
+
writeLine(context.stdout, "");
|
|
2359
|
+
writeLine(context.stdout, `Drafted copy: ${display(draftedStep.stage)}${branch.resolved_target_step ? ` (${branch.resolved_target_step})` : ""}`);
|
|
2360
|
+
const targetUrl = writerDraftTargetUrl(draftedStep);
|
|
2361
|
+
if (targetUrl) writeLine(context.stdout, `Replying to: ${targetUrl}`);
|
|
2362
|
+
if (draftedStep.subject) writeLine(context.stdout, `Subject: ${draftedStep.subject}`);
|
|
2363
|
+
writeLine(context.stdout, display(draftedStep.body || draftedStep.text || draftedStep.empty_body_reason || ""));
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
function writerDraftTargetUrl(step) {
|
|
2367
|
+
const target = step?.target || {};
|
|
2368
|
+
return target.post_url || target.comment_url || target.url || null;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function writerStepDate(step, context) {
|
|
2372
|
+
if (step?.timing?.mode === "scheduled") return dateOnlyLabel(step?.timing?.scheduled_for);
|
|
2373
|
+
return String(display(step.kind)).toLowerCase() === "terminal" ? "after" : isoDate(currentDate(context));
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
function writerStepDow(dateLabel) {
|
|
2377
|
+
const date = parseDateOnlyLabel(dateLabel);
|
|
2378
|
+
return date ? DAY_OF_WEEK_LABELS[date.getUTCDay()] : "";
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function dateOnlyLabel(value) {
|
|
2382
|
+
const raw = String(display(value)).trim();
|
|
2383
|
+
return raw.match(/^\d{4}-\d{2}-\d{2}/)?.[0] || raw;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
function parseDateOnlyLabel(value) {
|
|
2387
|
+
const match = String(display(value)).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
2388
|
+
if (!match) return null;
|
|
2389
|
+
|
|
2390
|
+
const [, year, month, day] = match;
|
|
2391
|
+
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
function writerStepAction(step) {
|
|
2395
|
+
return display(step.stage || step.key);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function compactKindLabel(kind) {
|
|
2399
|
+
const value = String(display(kind)).toLowerCase();
|
|
2400
|
+
if (value === "message") return "MSG";
|
|
2401
|
+
if (value === "action") return "ACT";
|
|
2402
|
+
if (value === "event") return "EVT";
|
|
2403
|
+
if (value === "terminal") return "END";
|
|
2404
|
+
if (value === "wait") return "WAIT";
|
|
2405
|
+
return display(kind).toUpperCase();
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function compactChannelLabel(channel) {
|
|
2409
|
+
const value = String(display(channel));
|
|
2410
|
+
if (value === "LinkedIn") return "LI";
|
|
2411
|
+
if (value === "LinkedIn InMail") return "InMail";
|
|
2412
|
+
if (value === "Timeline") return "Wait";
|
|
2413
|
+
if (value === "Disposition") return "Done";
|
|
2414
|
+
return value;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function fixedWidth(value, width, options = {}) {
|
|
2418
|
+
const text = truncateCliText(value, width);
|
|
2419
|
+
return options.align === "right" ? text.padStart(width) : text.padEnd(width);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function truncateCliText(value, maxLength) {
|
|
2423
|
+
const text = String(display(value)).replace(/\s+/g, " ").trim();
|
|
2424
|
+
if (text.length <= maxLength) return text;
|
|
2425
|
+
return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
function renderSequenceStep(step, index, context) {
|
|
2429
|
+
const kind = display(step.kind).toUpperCase();
|
|
2430
|
+
const stage = display(step.stage);
|
|
2431
|
+
const channel = display(step.channel);
|
|
2432
|
+
const timing = step?.timing?.mode === "scheduled" ? ` [scheduled ${display(step?.timing?.scheduled_for)}]` : "";
|
|
2433
|
+
writeLine(context.stdout, `${index + 1}. ${kind} | ${stage} | ${channel}${timing}`);
|
|
2434
|
+
|
|
2435
|
+
if (step.disposition) writeLine(context.stdout, ` Disposition: ${step.disposition}`);
|
|
2436
|
+
if (step.status) writeLine(context.stdout, ` Status: ${step.status}`);
|
|
2437
|
+
if (step.transition_label) writeLine(context.stdout, ` Transition: ${step.transition_label}`);
|
|
2438
|
+
if (step.rationale) writeLine(context.stdout, ` Why: ${step.rationale}`);
|
|
2439
|
+
if (step.guidance) writeLine(context.stdout, ` Guidance: ${step.guidance}`);
|
|
2440
|
+
if (step.subject) writeLine(context.stdout, ` Subject: ${step.subject}`);
|
|
2441
|
+
if (step.body) writeLine(context.stdout, ` Body: ${step.body}`);
|
|
2442
|
+
if (step.empty_body_reason) writeLine(context.stdout, ` Empty body reason: ${step.empty_body_reason}`);
|
|
2443
|
+
if (step.missing_reason) writeLine(context.stdout, ` Missing reason: ${step.missing_reason}`);
|
|
1820
2444
|
}
|
|
1821
2445
|
|
|
1822
2446
|
function renderProspectSequenceExport(payload, context) {
|
|
@@ -1899,17 +2523,13 @@ function renderOperatorQueue(payload, context) {
|
|
|
1899
2523
|
return;
|
|
1900
2524
|
}
|
|
1901
2525
|
|
|
1902
|
-
|
|
1903
|
-
for (const row of queue) {
|
|
1904
|
-
writeLine(context.stdout, operatorRowLine(row));
|
|
1905
|
-
}
|
|
2526
|
+
writeOperatorRows(context, queue);
|
|
1906
2527
|
}
|
|
1907
2528
|
|
|
1908
2529
|
function renderOperatorNext(row, context) {
|
|
1909
2530
|
if (!row) return writeLine(context.stdout, "No operator moves found.");
|
|
1910
2531
|
|
|
1911
|
-
|
|
1912
|
-
writeLine(context.stdout, operatorRowLine(row));
|
|
2532
|
+
writeOperatorRows(context, [row]);
|
|
1913
2533
|
}
|
|
1914
2534
|
|
|
1915
2535
|
function renderOperatorPlan(row, context) {
|
|
@@ -1981,11 +2601,72 @@ function renderOperatorOutcome(payload, context) {
|
|
|
1981
2601
|
|
|
1982
2602
|
function renderAnalyticsProspects(payload, context) {
|
|
1983
2603
|
writeLine(context.stdout, `Prospect analytics (${analyticsWindowLabel(payload)})`);
|
|
2604
|
+
writeAnalyticsCohort(payload, context);
|
|
1984
2605
|
writeAnalyticsScope(payload, context);
|
|
1985
|
-
|
|
2606
|
+
if (payload?.cohort) {
|
|
2607
|
+
writeLine(context.stdout, `Cohort prospects: ${display(payload?.cohort_prospects_count, payload?.prospects_added_count || 0)}`);
|
|
2608
|
+
} else {
|
|
2609
|
+
writeLine(context.stdout, `Prospects added: ${display(payload?.prospects_added_count, 0)}`);
|
|
2610
|
+
}
|
|
1986
2611
|
writeAnalyticsActionSummary(payload?.actions, context, "Actions");
|
|
1987
2612
|
writeCountTable(context, "Action breakdown", payload?.actions?.breakdown, ["ACTION", "COUNT", "AUTOMATED", "AUTO %"], actionBreakdownRow);
|
|
1988
|
-
writeCountTable(context, "Queue stages", payload?.queue_stages, ["STAGE", "COUNT"], countRow);
|
|
2613
|
+
writeCountTable(context, payload?.cohort ? "Current cohort stages" : "Queue stages", payload?.queue_stages, ["STAGE", "COUNT"], countRow);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
function renderMotionAnalytics(payload, context) {
|
|
2617
|
+
writeLine(context.stdout, `Motion analytics (${analyticsWindowLabel(payload)})`);
|
|
2618
|
+
writeAnalyticsMotion(payload, context);
|
|
2619
|
+
if (payload?.motion?.created_at) writeLine(context.stdout, `Created: ${payload.motion.created_at}`);
|
|
2620
|
+
writeLine(context.stdout, `Prospects produced: ${display(payload?.prospects_added_count, 0)}`);
|
|
2621
|
+
writeCountTable(context, "Prospect cohorts by produced day", payload?.prospects_by_day, ["DATE", "PRODUCED", "ACTIVE", "ACTIVE %", "INACTIVE", "STAGES"], dailyProspectRow);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
function renderAnalyticsProspectCohortAnalysis(payload, context) {
|
|
2625
|
+
const cohorts = Array.isArray(payload?.cohorts) ? payload.cohorts : [];
|
|
2626
|
+
writeLine(context.stdout, `Prospect cohort analysis (${display(payload?.weeks, cohorts.length)} weeks)`);
|
|
2627
|
+
writeLine(context.stdout, `Activity window: ${display(payload?.window, "24h")}`);
|
|
2628
|
+
writeLine(context.stdout, "Cohorts: account_prospects.created_at, calendar weeks, oldest first");
|
|
2629
|
+
writeAnalyticsMotion(payload, context);
|
|
2630
|
+
writeAnalyticsProvenance(payload, context);
|
|
2631
|
+
if (payload?.account_user) {
|
|
2632
|
+
writeLine(context.stdout, `User: ${entityLabel(payload.account_user)}`);
|
|
2633
|
+
} else {
|
|
2634
|
+
writeLine(context.stdout, "User: all account users");
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
if (cohorts.length === 0) {
|
|
2638
|
+
writeLine(context.stdout, "No cohorts generated.");
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
const stageColumns = cohortAnalysisStageColumns(cohorts);
|
|
2643
|
+
const headers = ["COHORT", "TOTAL", ...stageColumns.map((column) => column.label)];
|
|
2644
|
+
const rows = cohorts.map((row) => [
|
|
2645
|
+
row.label,
|
|
2646
|
+
row.total_count,
|
|
2647
|
+
...stageColumns.map((column) => row.stages?.[column.key] || 0)
|
|
2648
|
+
]);
|
|
2649
|
+
|
|
2650
|
+
writeLine(context.stdout, "");
|
|
2651
|
+
writeAlignedTable(context, headers, rows, {
|
|
2652
|
+
numericColumns: headers.map((_, index) => index > 0)
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
function renderAnalyticsUsers(payload, context) {
|
|
2657
|
+
writeLine(context.stdout, `User analytics (${analyticsActivityLabel(payload)})`);
|
|
2658
|
+
writeAnalyticsCohort(payload, context);
|
|
2659
|
+
writeAnalyticsScope(payload, context);
|
|
2660
|
+
writeAnalyticsPlatform(payload, context);
|
|
2661
|
+
|
|
2662
|
+
const summary = payload?.summary || {};
|
|
2663
|
+
writeLine(context.stdout, `Actions: ${display(summary.total_count, 0)}`);
|
|
2664
|
+
writeLine(context.stdout, `Performed by you: ${display(summary.performed_by_user_count, 0)} (${percentageLabel(summary.performed_by_user_percentage)})`);
|
|
2665
|
+
writeLine(context.stdout, `Other humans: ${display(summary.performed_by_others_count, 0)} (${percentageLabel(summary.performed_by_others_percentage)})`);
|
|
2666
|
+
writeLine(context.stdout, `Agent: ${display(summary.agentic_count, 0)} (${percentageLabel(summary.agentic_percentage)})`);
|
|
2667
|
+
writeAnalyticsDailyActions(payload, context);
|
|
2668
|
+
writeCountTable(context, "Action mix", payload?.action_mix, ["ACTION", "COUNT", "%"], mixRow);
|
|
2669
|
+
writeCountTable(context, "Platform mix", payload?.platform_mix, ["PLATFORM", "COUNT", "%"], mixRow);
|
|
1989
2670
|
}
|
|
1990
2671
|
|
|
1991
2672
|
function renderAnalyticsVisibility(payload, context) {
|
|
@@ -2005,6 +2686,8 @@ function renderAnalyticsContent(payload, context) {
|
|
|
2005
2686
|
}
|
|
2006
2687
|
|
|
2007
2688
|
function writeAnalyticsScope(payload, context) {
|
|
2689
|
+
writeAnalyticsMotion(payload, context);
|
|
2690
|
+
writeAnalyticsProvenance(payload, context);
|
|
2008
2691
|
if (payload?.account_user) {
|
|
2009
2692
|
writeLine(context.stdout, `User: ${entityLabel(payload.account_user)}`);
|
|
2010
2693
|
} else {
|
|
@@ -2012,6 +2695,113 @@ function writeAnalyticsScope(payload, context) {
|
|
|
2012
2695
|
}
|
|
2013
2696
|
}
|
|
2014
2697
|
|
|
2698
|
+
function writeAnalyticsPlatform(payload, context) {
|
|
2699
|
+
if (!payload?.platform) return;
|
|
2700
|
+
|
|
2701
|
+
const label = display(payload.platform.label, payload.platform.key);
|
|
2702
|
+
const values = Array.isArray(payload.platform.values) ? payload.platform.values.filter(Boolean).join(", ") : display(payload.platform.key);
|
|
2703
|
+
writeLine(context.stdout, `Platform: ${label} (${display(payload.platform.field, "events.platform")}: ${values})`);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
function writeAnalyticsMotion(payload, context) {
|
|
2707
|
+
if (!payload?.motion) return;
|
|
2708
|
+
|
|
2709
|
+
writeLine(context.stdout, `Motion: ${entityLabel(payload.motion)}`);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
function writeAnalyticsProvenance(payload, context) {
|
|
2713
|
+
if (!payload?.provenance) return;
|
|
2714
|
+
|
|
2715
|
+
writeLine(context.stdout, `Provenance: ${display(payload.provenance.label, payload.provenance.key)} (${display(payload.provenance.field, "account_prospects.intake_source")})`);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
function writeAnalyticsCohort(payload, context) {
|
|
2719
|
+
const cohort = payload?.cohort;
|
|
2720
|
+
if (!cohort) return;
|
|
2721
|
+
|
|
2722
|
+
writeLine(context.stdout, `Cohort: ${display(cohort.start_date)} to ${display(cohort.end_date)} (${display(cohort.field, "account_prospects.created_at")})`);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
function writeAnalyticsDailyActions(payload, context) {
|
|
2726
|
+
const dailyRows = Array.isArray(payload?.daily_actions) ? payload.daily_actions : [];
|
|
2727
|
+
writeLine(context.stdout, "");
|
|
2728
|
+
writeLine(context.stdout, "Actions by day");
|
|
2729
|
+
if (dailyRows.length === 0) return writeLine(context.stdout, "None");
|
|
2730
|
+
|
|
2731
|
+
const actionColumns = dailyActionColumns(dailyRows, payload?.action_mix);
|
|
2732
|
+
const headers = ["DATE", "TOTAL", ...actionColumns.map((column) => column.label)];
|
|
2733
|
+
const rows = dailyRows.map((row) => [
|
|
2734
|
+
row?.date,
|
|
2735
|
+
row?.total_count || 0,
|
|
2736
|
+
...actionColumns.map((column) => row?.actions?.[column.key] || 0)
|
|
2737
|
+
]);
|
|
2738
|
+
|
|
2739
|
+
writeAlignedTable(context, headers, rows, {
|
|
2740
|
+
numericColumns: headers.map((_, index) => index > 0)
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
function dailyActionColumns(dailyRows, actionMix) {
|
|
2745
|
+
const labels = {};
|
|
2746
|
+
const keys = [];
|
|
2747
|
+
for (const row of Array.isArray(actionMix) ? actionMix : []) {
|
|
2748
|
+
const key = String(row?.key || "").trim();
|
|
2749
|
+
if (!key) continue;
|
|
2750
|
+
if (!keys.includes(key)) keys.push(key);
|
|
2751
|
+
labels[key] = row?.label || key;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
for (const row of dailyRows) {
|
|
2755
|
+
for (const key of Object.keys(row?.actions || {})) {
|
|
2756
|
+
if (!keys.includes(key)) keys.push(key);
|
|
2757
|
+
labels[key] ||= key;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
return keys.slice(0, 6).map((key) => ({ key, label: compactActionLabel(key, labels[key] || key) }));
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
function compactActionLabel(key, label) {
|
|
2765
|
+
const labels = {
|
|
2766
|
+
"action.profile.connect_request_sent": "Connect sent",
|
|
2767
|
+
"action.profile.withdraw_connection": "Withdraw",
|
|
2768
|
+
"action.profile.follow": "Follow",
|
|
2769
|
+
"action.profile.view": "View",
|
|
2770
|
+
"action.profile.in_mail_message": "InMail",
|
|
2771
|
+
"action.post.comment": "Comment",
|
|
2772
|
+
"action.post.like": "Like",
|
|
2773
|
+
"messaging.message_sent": "Message",
|
|
2774
|
+
"messaging.email_sent": "Email",
|
|
2775
|
+
"action.meeting.requested": "Meeting req",
|
|
2776
|
+
"action.prospect.nurtured": "Nurtured",
|
|
2777
|
+
"action.prospect.motion_completed_no_outcome": "No outcome"
|
|
2778
|
+
};
|
|
2779
|
+
if (labels[key]) return labels[key];
|
|
2780
|
+
|
|
2781
|
+
const words = String(label || key).split(/\s+/).filter(Boolean);
|
|
2782
|
+
return words.length <= 2 ? words.join(" ") : words.slice(0, 2).join(" ");
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
function cohortAnalysisStageColumns(cohorts) {
|
|
2786
|
+
const labels = {};
|
|
2787
|
+
const keys = [];
|
|
2788
|
+
for (const cohort of cohorts) {
|
|
2789
|
+
for (const [key, label] of Object.entries(cohort.stage_labels || {})) {
|
|
2790
|
+
if (!keys.includes(key)) keys.push(key);
|
|
2791
|
+
labels[key] ||= label;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
return keys
|
|
2796
|
+
.sort((left, right) => cohortStageRank(left) - cohortStageRank(right) || left.localeCompare(right))
|
|
2797
|
+
.map((key) => ({ key, label: labels[key] || key }));
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
function cohortStageRank(key) {
|
|
2801
|
+
const index = COHORT_STAGE_ORDER.indexOf(String(key));
|
|
2802
|
+
return index === -1 ? COHORT_STAGE_ORDER.length : index;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2015
2805
|
function writeAnalyticsActionSummary(actions, context, label) {
|
|
2016
2806
|
const total = display(actions?.total_count, 0);
|
|
2017
2807
|
const automated = display(actions?.automated_count, 0);
|
|
@@ -2025,8 +2815,32 @@ function writeCountTable(context, title, rows, headers, mapRow) {
|
|
|
2025
2815
|
writeLine(context.stdout, title);
|
|
2026
2816
|
if (list.length === 0) return writeLine(context.stdout, "None");
|
|
2027
2817
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2818
|
+
writeAlignedTable(context, headers, list.map(mapRow));
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function writeAlignedTable(context, headers, rows, options = {}) {
|
|
2822
|
+
const tableRows = [headers, ...rows].map((row) => row.map((value) => display(value)));
|
|
2823
|
+
const widths = headers.map((_, index) => Math.max(...tableRows.map((row) => visibleLength(row[index] || ""))));
|
|
2824
|
+
const numericColumns = options.numericColumns || headers.map((header, index) => index > 0 && numericHeader(header));
|
|
2825
|
+
|
|
2826
|
+
writeLine(context.stdout, formatAlignedRow(headers, widths, numericColumns));
|
|
2827
|
+
writeLine(context.stdout, widths.map((width) => "-".repeat(width)).join(" "));
|
|
2828
|
+
for (const row of rows) writeLine(context.stdout, formatAlignedRow(row, widths, numericColumns));
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
function numericHeader(header) {
|
|
2832
|
+
return ["COUNT", "AUTOMATED", "AUTO %", "TOTAL", "%", "PRODUCED", "ACTIVE", "ACTIVE %", "INACTIVE"].includes(String(header || "").toUpperCase());
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
function formatAlignedRow(row, widths, numericColumns) {
|
|
2836
|
+
return row.map((value, index) => {
|
|
2837
|
+
const text = String(display(value));
|
|
2838
|
+
return numericColumns[index] ? text.padStart(widths[index]) : text.padEnd(widths[index]);
|
|
2839
|
+
}).join(" ");
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
function visibleLength(value) {
|
|
2843
|
+
return String(display(value)).length;
|
|
2030
2844
|
}
|
|
2031
2845
|
|
|
2032
2846
|
function actionBreakdownRow(row) {
|
|
@@ -2045,6 +2859,37 @@ function countRow(row) {
|
|
|
2045
2859
|
];
|
|
2046
2860
|
}
|
|
2047
2861
|
|
|
2862
|
+
function dailyProspectRow(row) {
|
|
2863
|
+
const count = Number(row?.count || 0);
|
|
2864
|
+
return [
|
|
2865
|
+
display(row?.date),
|
|
2866
|
+
countDash(row?.count),
|
|
2867
|
+
countDash(row?.active_count),
|
|
2868
|
+
count > 0 ? percentageLabel(row?.active_percentage) : "-",
|
|
2869
|
+
countDash(row?.inactive_count),
|
|
2870
|
+
stageSummary(row?.queue_stages)
|
|
2871
|
+
];
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
function countDash(value) {
|
|
2875
|
+
return Number(value || 0) === 0 ? "-" : display(value, 0);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
function stageSummary(rows) {
|
|
2879
|
+
const stages = Array.isArray(rows) ? rows.filter((row) => Number(row?.count || 0) > 0) : [];
|
|
2880
|
+
if (stages.length === 0) return "-";
|
|
2881
|
+
|
|
2882
|
+
return stages.map((row) => `${display(row?.label || row?.key)} ${display(row?.count, 0)}`).join(" | ");
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
function mixRow(row) {
|
|
2886
|
+
return [
|
|
2887
|
+
display(row?.label || row?.key),
|
|
2888
|
+
display(row?.count, 0),
|
|
2889
|
+
percentageLabel(row?.percentage)
|
|
2890
|
+
];
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2048
2893
|
function analyticsWindowLabel(payload) {
|
|
2049
2894
|
const window = payload?.window || {};
|
|
2050
2895
|
const key = display(window.key, "24h");
|
|
@@ -2053,18 +2898,67 @@ function analyticsWindowLabel(payload) {
|
|
|
2053
2898
|
return `${key}: ${window.started_at} to ${window.ended_at}`;
|
|
2054
2899
|
}
|
|
2055
2900
|
|
|
2901
|
+
function analyticsActivityLabel(payload) {
|
|
2902
|
+
const range = payload?.date_range;
|
|
2903
|
+
if (range?.start_date && range?.end_date) {
|
|
2904
|
+
return `${range.start_date} to ${range.end_date}`;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
return analyticsWindowLabel(payload);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2056
2910
|
function percentageLabel(value) {
|
|
2057
2911
|
return value === undefined || value === null || value === "" ? "n/a" : `${value}%`;
|
|
2058
2912
|
}
|
|
2059
2913
|
|
|
2060
|
-
function
|
|
2914
|
+
function humanize(value) {
|
|
2915
|
+
const words = String(value || "").trim().replaceAll("-", "_").split("_").filter(Boolean);
|
|
2916
|
+
if (words.length === 0) return "-";
|
|
2917
|
+
|
|
2918
|
+
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
function writeOperatorRows(context, rows) {
|
|
2922
|
+
writeAlignedTable(context, ["MOVE ID", "WORK TYPE", "SUBJECT", "MOTION", "NEXT ACTION"], rows.map(operatorTableRow));
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
function operatorTableRow(row) {
|
|
2061
2926
|
return [
|
|
2062
2927
|
display(row?.id),
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2928
|
+
operatorWorkTypeLabel(row),
|
|
2929
|
+
operatorSubjectLabel(row),
|
|
2930
|
+
operatorMotionLabel(row),
|
|
2066
2931
|
display(nextActionLabel(row))
|
|
2067
|
-
]
|
|
2932
|
+
];
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
function operatorWorkTypeLabel(row) {
|
|
2936
|
+
return humanize(row?.opportunity_kind || row?.source_kind);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
function operatorSubjectLabel(row) {
|
|
2940
|
+
return display(
|
|
2941
|
+
row?.prospect?.display_name ||
|
|
2942
|
+
row?.prospect?.name ||
|
|
2943
|
+
row?.profile?.display_name ||
|
|
2944
|
+
row?.profile?.username ||
|
|
2945
|
+
postLabel(row?.post) ||
|
|
2946
|
+
row?.display_name ||
|
|
2947
|
+
row?.name
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function operatorMotionLabel(row) {
|
|
2952
|
+
return display(row?.motion?.name || row?.motion?.display_name || row?.motion?.prefix_id || row?.motion?.id);
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
function postLabel(post) {
|
|
2956
|
+
if (!post) return "";
|
|
2957
|
+
|
|
2958
|
+
const body = String(post.body || "").trim().replace(/\s+/g, " ");
|
|
2959
|
+
if (body) return body.length > 48 ? `${body.slice(0, 45)}...` : body;
|
|
2960
|
+
|
|
2961
|
+
return post.url || (post.id ? `Post ${post.id}` : "");
|
|
2068
2962
|
}
|
|
2069
2963
|
|
|
2070
2964
|
function nextActionLabel(source) {
|
|
@@ -2329,65 +3223,77 @@ const HELP_TOPICS = new Map([
|
|
|
2329
3223
|
"Usage:",
|
|
2330
3224
|
" audienti <command> [options]",
|
|
2331
3225
|
"",
|
|
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
|
-
"
|
|
3226
|
+
"Start:",
|
|
3227
|
+
" audienti auth token <token> Save an API token",
|
|
3228
|
+
" audienti accounts list See accounts available to this token",
|
|
3229
|
+
" audienti accounts select <acct_id> Use one account by default",
|
|
3230
|
+
" audienti help agent-workflows Common agent/operator paths",
|
|
3231
|
+
"",
|
|
3232
|
+
"Work areas:",
|
|
3233
|
+
" Setup & identity",
|
|
3234
|
+
" audienti auth status",
|
|
3235
|
+
" audienti config list",
|
|
3236
|
+
" audienti users list",
|
|
3237
|
+
"",
|
|
3238
|
+
" Motions / plays",
|
|
3239
|
+
" audienti motions list",
|
|
3240
|
+
" audienti motions show <motn_id>",
|
|
3241
|
+
" audienti motions analytics <motn_id>",
|
|
3242
|
+
" audienti motions prospects <motn_id>",
|
|
3243
|
+
" audienti motions create --payload <file.json>",
|
|
3244
|
+
" Tip: `plays` is accepted anywhere `motions` is accepted.",
|
|
3245
|
+
"",
|
|
3246
|
+
" Prospects",
|
|
3247
|
+
" audienti prospects list [filters]",
|
|
3248
|
+
" audienti prospects show <prsp_id>",
|
|
3249
|
+
" audienti prospects timeline <prsp_id>",
|
|
3250
|
+
" audienti prospects import <linkedin_url> [--motion <motn_id>]",
|
|
3251
|
+
" audienti prospects add-note <prsp_id> --message <text>",
|
|
3252
|
+
" audienti prospects add-profile <prsp_id> --url <profile_url|email|phone>",
|
|
3253
|
+
"",
|
|
3254
|
+
" Lists & targeting inputs",
|
|
3255
|
+
" audienti lists list",
|
|
3256
|
+
" audienti lists prospects <list_id>",
|
|
3257
|
+
" audienti offers list",
|
|
3258
|
+
" audienti icps list",
|
|
3259
|
+
" audienti companies search --query <text>",
|
|
3260
|
+
"",
|
|
3261
|
+
" Writer",
|
|
3262
|
+
" audienti writer test-run <prsp_id>",
|
|
3263
|
+
" audienti prospects write <prsp_id> --type <surface_key>",
|
|
3264
|
+
" audienti prospects sequence-export <prsp_id>",
|
|
3265
|
+
"",
|
|
3266
|
+
" Operator queue",
|
|
3267
|
+
" audienti operator next --plan",
|
|
3268
|
+
" audienti operator next --done --note <text>",
|
|
3269
|
+
" audienti operator queue",
|
|
3270
|
+
"",
|
|
3271
|
+
" Analytics",
|
|
3272
|
+
" audienti analytics prospects --window 24h",
|
|
3273
|
+
" audienti analytics prospects cohort-analysis --weeks 4 --motion <motn_id>",
|
|
3274
|
+
" audienti analytics users --user me --window 30d",
|
|
3275
|
+
" audienti analytics visibility --window 24h --user me",
|
|
3276
|
+
" audienti analytics content --window week",
|
|
3277
|
+
"",
|
|
3278
|
+
" Utilities",
|
|
3279
|
+
" audienti tools get email --url <linkedin_url>",
|
|
3280
|
+
" audienti tools get phone --url <linkedin_url>",
|
|
3281
|
+
"",
|
|
3282
|
+
"Common flows:",
|
|
3283
|
+
" Work the next move: audienti operator next --plan",
|
|
3284
|
+
" Inspect a prospect: audienti prospects show <prsp_id> --json",
|
|
3285
|
+
" Preview a campaign: audienti writer test-run <prsp_id>",
|
|
3286
|
+
" Analyze one motion: audienti motions analytics <motn_id>",
|
|
3287
|
+
" Audit your work: audienti analytics users --user me --window 30d",
|
|
2385
3288
|
"",
|
|
2386
3289
|
"Global options:",
|
|
2387
3290
|
" --account <acct_id> Use an account for one command without saving it",
|
|
2388
3291
|
" --help, -h Show help",
|
|
2389
3292
|
"",
|
|
2390
|
-
"
|
|
3293
|
+
"More help:",
|
|
3294
|
+
" audienti <area> help Example: audienti prospects help",
|
|
3295
|
+
" audienti <area> <command> help Example: audienti analytics prospects help",
|
|
3296
|
+
" Use --json when another program or agent will consume the output."
|
|
2391
3297
|
].join("\n")],
|
|
2392
3298
|
|
|
2393
3299
|
["auth", [
|
|
@@ -2903,6 +3809,7 @@ const HELP_TOPICS = new Map([
|
|
|
2903
3809
|
" audienti motions list [--json]",
|
|
2904
3810
|
" audienti motions show <motn_id> [--json]",
|
|
2905
3811
|
" audienti motions status <motn_id> [--json]",
|
|
3812
|
+
" audienti motions analytics <motn_id> [--window 30d] [--json]",
|
|
2906
3813
|
" audienti motions prospects <motn_id> [--json]",
|
|
2907
3814
|
" audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
|
|
2908
3815
|
" audienti motions create --payload <file.json> [--json]",
|
|
@@ -2971,6 +3878,27 @@ const HELP_TOPICS = new Map([
|
|
|
2971
3878
|
" GET /api/v1/accounts/:account_id/motions/:id/status.json"
|
|
2972
3879
|
].join("\n")],
|
|
2973
3880
|
|
|
3881
|
+
["motions analytics", [
|
|
3882
|
+
"Usage:",
|
|
3883
|
+
` ${MOTIONS_ANALYTICS_USAGE.slice("Usage: ".length)}`,
|
|
3884
|
+
"",
|
|
3885
|
+
"Status: implemented",
|
|
3886
|
+
"",
|
|
3887
|
+
"Purpose:",
|
|
3888
|
+
" Show whether one motion is producing prospect output by day.",
|
|
3889
|
+
"",
|
|
3890
|
+
"Options:",
|
|
3891
|
+
" --window <w> AccountProspect.created_at window to inspect. Defaults to 30d. Maximum 90d.",
|
|
3892
|
+
"",
|
|
3893
|
+
"Output shape:",
|
|
3894
|
+
" motion: selected motion/play, including created_at",
|
|
3895
|
+
" prospects_added_count: prospects produced inside the window",
|
|
3896
|
+
" prospects_by_day[]: date, count, active/inactive counts, and current queue_stages for that produced-day cohort",
|
|
3897
|
+
"",
|
|
3898
|
+
"API:",
|
|
3899
|
+
" GET /api/v1/accounts/:account_id/analytics/prospects.json?motion_id=:motion_id"
|
|
3900
|
+
].join("\n")],
|
|
3901
|
+
|
|
2974
3902
|
["motions prospects", [
|
|
2975
3903
|
"Usage:",
|
|
2976
3904
|
" audienti motions prospects <motn_id> [--json] [--account <acct_id>]",
|
|
@@ -3358,9 +4286,54 @@ const HELP_TOPICS = new Map([
|
|
|
3358
4286
|
" POST /api/v1/accounts/:account_id/prospects/:id/sequence_preview.json"
|
|
3359
4287
|
].join("\n")],
|
|
3360
4288
|
|
|
4289
|
+
["writer", [
|
|
4290
|
+
"Usage:",
|
|
4291
|
+
` ${WRITER_TEST_RUN_USAGE.slice("Usage: ".length)}`,
|
|
4292
|
+
"",
|
|
4293
|
+
"Status: implemented",
|
|
4294
|
+
"",
|
|
4295
|
+
"Purpose:",
|
|
4296
|
+
" Run a writer campaign test for one prospect: resolve their current context, simulate the full no-reply path, and draft each message step.",
|
|
4297
|
+
"",
|
|
4298
|
+
"Commands:",
|
|
4299
|
+
" audienti writer test-run <prsp_id>",
|
|
4300
|
+
"",
|
|
4301
|
+
"Alias:",
|
|
4302
|
+
" audienti writers test-run <prsp_id>"
|
|
4303
|
+
].join("\n")],
|
|
4304
|
+
|
|
4305
|
+
["writer test-run", [
|
|
4306
|
+
"Usage:",
|
|
4307
|
+
` ${WRITER_TEST_RUN_USAGE.slice("Usage: ".length)}`,
|
|
4308
|
+
"",
|
|
4309
|
+
"Status: implemented",
|
|
4310
|
+
"",
|
|
4311
|
+
"Purpose:",
|
|
4312
|
+
" Run the prospect-scoped writer test run for a single prospect.",
|
|
4313
|
+
"",
|
|
4314
|
+
"Behavior:",
|
|
4315
|
+
" 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.",
|
|
4316
|
+
"",
|
|
4317
|
+
"Options:",
|
|
4318
|
+
" --mode <mode> report drafts every message, plan skips drafting, step drafts one selected step",
|
|
4319
|
+
" --branch <branch> Optional branch filter: both | no-accept | accepted",
|
|
4320
|
+
" --step <step_key|row_number> Required with --mode step. Row numbers come from the # column.",
|
|
4321
|
+
" --no-cache Plan/step modes ignore locally cached simulator drafts",
|
|
4322
|
+
" --clear-cache Plan/step modes clear locally cached simulator drafts before running",
|
|
4323
|
+
"",
|
|
4324
|
+
"Output shape:",
|
|
4325
|
+
" branches[].key: no_accept | accepted",
|
|
4326
|
+
" branches[].steps[]: ordered wait/action/message/terminal steps for that simulated path",
|
|
4327
|
+
" branches[].steps[].body: draft copy for message steps when the writer can generate one",
|
|
4328
|
+
" branches[].summary: channel sequence, touch counts, duration, terminal disposition",
|
|
4329
|
+
"",
|
|
4330
|
+
"API:",
|
|
4331
|
+
" POST /api/v1/accounts/:account_id/prospects/:id/sequence_export.json"
|
|
4332
|
+
].join("\n")],
|
|
4333
|
+
|
|
3361
4334
|
["prospects sequence-export", [
|
|
3362
4335
|
"Usage:",
|
|
3363
|
-
" audienti prospects sequence-export <prsp_id> [--json|--csv] [--branch <both|no-accept|accepted>] [--angle-index <n>] [--account <acct_id>]",
|
|
4336
|
+
" 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
4337
|
"",
|
|
3365
4338
|
"Status: implemented",
|
|
3366
4339
|
"",
|
|
@@ -3372,6 +4345,11 @@ const HELP_TOPICS = new Map([
|
|
|
3372
4345
|
" no-accept: no reply and no accepted connection request",
|
|
3373
4346
|
" accepted: connection request accepted, then no reply otherwise",
|
|
3374
4347
|
"",
|
|
4348
|
+
"Draft modes:",
|
|
4349
|
+
" all: default. Draft every message step",
|
|
4350
|
+
" plan: build the timeline without drafting message bodies",
|
|
4351
|
+
" target: draft only --target-step and return the branch prefix through that step. Row numbers use rows[].step_number.",
|
|
4352
|
+
"",
|
|
3375
4353
|
"Output shape:",
|
|
3376
4354
|
" rows[].prospect_id: prsp_",
|
|
3377
4355
|
" rows[].branch: no_accept | accepted",
|
|
@@ -3590,7 +4568,9 @@ const HELP_TOPICS = new Map([
|
|
|
3590
4568
|
|
|
3591
4569
|
["analytics", [
|
|
3592
4570
|
"Usage:",
|
|
3593
|
-
" audienti analytics prospects [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
4571
|
+
" 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]",
|
|
4572
|
+
" audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--user <account_user_id|email|name|me>] [--json]",
|
|
4573
|
+
" 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
4574
|
" audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
3595
4575
|
" audienti analytics visops [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
3596
4576
|
" audienti analytics content [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
|
|
@@ -3599,23 +4579,56 @@ const HELP_TOPICS = new Map([
|
|
|
3599
4579
|
"",
|
|
3600
4580
|
"Window:",
|
|
3601
4581
|
" --window <24h|7d|1w|day|week>",
|
|
3602
|
-
" --
|
|
4582
|
+
" --start <YYYY-MM-DD> --end <YYYY-MM-DD> For user analytics, select the events.created_at activity range instead of --window.",
|
|
4583
|
+
" --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.",
|
|
4584
|
+
" --motion <motn_id> For prospect and user analytics, filter AccountProspect.motion_id to one motion/play.",
|
|
4585
|
+
" --provenance <source> Optional lower-level AccountProspect.intake_source filter.",
|
|
4586
|
+
" --platform <linkedin|email|gmail> For user analytics, filter events.platform. --channel is accepted as an alias.",
|
|
4587
|
+
" cohort-analysis loops over recent weekly AccountProspect.created_at cohorts and compares their current stages.",
|
|
4588
|
+
" --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
4589
|
"",
|
|
3604
4590
|
"Output:",
|
|
3605
|
-
" Account-scoped analytics for prospects, visibility engagement, and ContentOps publishing."
|
|
4591
|
+
" Account-scoped analytics for prospects, users, visibility engagement, and ContentOps publishing."
|
|
3606
4592
|
].join("\n")],
|
|
3607
4593
|
|
|
3608
4594
|
["analytics prospects", [
|
|
3609
4595
|
"Usage:",
|
|
3610
|
-
" audienti analytics prospects [--window 24h] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]",
|
|
4596
|
+
" 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>]",
|
|
4597
|
+
" 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
4598
|
"",
|
|
3612
4599
|
"Status: implemented",
|
|
3613
4600
|
"",
|
|
3614
4601
|
"Output shape:",
|
|
3615
|
-
"
|
|
4602
|
+
" window: activity/event period for actions",
|
|
4603
|
+
" cohort: selected AccountProspect.created_at cohort when cohort dates are provided",
|
|
4604
|
+
" motion: selected motion/play when --motion is provided",
|
|
4605
|
+
" provenance: selected AccountProspect.intake_source when --provenance is provided",
|
|
4606
|
+
" prospects_added_count: account prospects added in the window, or cohort size when cohort dates are provided",
|
|
4607
|
+
" cohort_prospects_count: selected AccountProspect.created_at cohort size when cohort dates are provided",
|
|
3616
4608
|
" account_user: selected account user when --user is provided, otherwise null",
|
|
3617
|
-
"
|
|
3618
|
-
"
|
|
4609
|
+
" --user filters AccountProspect.assigned_to_account_user_id, so `--user me` reports prospects assigned to you",
|
|
4610
|
+
" actions: outbound action totals in the window, narrowed to cohort prospects when cohort dates are provided",
|
|
4611
|
+
" queue_stages[]: current account prospect stage counts, narrowed to the selected cohort when cohort dates are provided",
|
|
4612
|
+
"",
|
|
4613
|
+
"API:",
|
|
4614
|
+
" GET /api/v1/accounts/:account_id/analytics/prospects.json"
|
|
4615
|
+
].join("\n")],
|
|
4616
|
+
|
|
4617
|
+
["analytics prospects cohort-analysis", [
|
|
4618
|
+
"Usage:",
|
|
4619
|
+
` ${ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE.slice("Usage: ".length)}`,
|
|
4620
|
+
"",
|
|
4621
|
+
"Status: implemented",
|
|
4622
|
+
"",
|
|
4623
|
+
"Behavior:",
|
|
4624
|
+
" 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.",
|
|
4625
|
+
"",
|
|
4626
|
+
"Options:",
|
|
4627
|
+
" --weeks <n> Number of calendar-week cohorts to inspect. Defaults to 4. Maximum 26.",
|
|
4628
|
+
" --window <w> Activity window passed through to each analytics call. Defaults to 24h.",
|
|
4629
|
+
" --motion <motn_id> Optional motion/play filter.",
|
|
4630
|
+
" --provenance <source> Optional AccountProspect.intake_source filter.",
|
|
4631
|
+
" --user <id> Optional account-user filter.",
|
|
3619
4632
|
"",
|
|
3620
4633
|
"API:",
|
|
3621
4634
|
" GET /api/v1/accounts/:account_id/analytics/prospects.json"
|
|
@@ -3628,6 +4641,43 @@ const HELP_TOPICS = new Map([
|
|
|
3628
4641
|
"Alias for `audienti analytics prospects`."
|
|
3629
4642
|
].join("\n")],
|
|
3630
4643
|
|
|
4644
|
+
["analytics users", [
|
|
4645
|
+
"Usage:",
|
|
4646
|
+
` ${ANALYTICS_USERS_USAGE.slice("Usage: ".length)}`,
|
|
4647
|
+
"",
|
|
4648
|
+
"Status: implemented",
|
|
4649
|
+
"",
|
|
4650
|
+
"Purpose:",
|
|
4651
|
+
" Audit one account user's outbound action history with the same actor semantics used by the Operations user analytics page.",
|
|
4652
|
+
"",
|
|
4653
|
+
"Options:",
|
|
4654
|
+
" --user <account_user_id|email|name|me> Defaults to me.",
|
|
4655
|
+
" --window <w> Activity window. Defaults to 30d when --start/--end are not provided.",
|
|
4656
|
+
" --start <YYYY-MM-DD> --end <YYYY-MM-DD> Explicit events.created_at activity range.",
|
|
4657
|
+
" --cohort-start <YYYY-MM-DD> --cohort-end <YYYY-MM-DD> Optional AccountProspect.created_at cohort filter.",
|
|
4658
|
+
" --motion <motn_id> Optional motion/play filter.",
|
|
4659
|
+
" --provenance <source> Optional AccountProspect.intake_source filter.",
|
|
4660
|
+
" --platform <linkedin|email|gmail> Optional events.platform filter. `email` includes email and gmail rows; --channel is an alias.",
|
|
4661
|
+
"",
|
|
4662
|
+
"Output shape:",
|
|
4663
|
+
" account_user: selected account user",
|
|
4664
|
+
" summary: performed-by-user totals and performed-by-others comparison",
|
|
4665
|
+
" daily_actions[]: action counts by events.created_at date",
|
|
4666
|
+
" action_mix[]: action type counts and percentages",
|
|
4667
|
+
" platform: selected platform/channel filter when --platform or --channel is provided",
|
|
4668
|
+
" platform_mix[]: platform counts and percentages",
|
|
4669
|
+
"",
|
|
4670
|
+
"API:",
|
|
4671
|
+
" GET /api/v1/accounts/:account_id/analytics/users.json"
|
|
4672
|
+
].join("\n")],
|
|
4673
|
+
|
|
4674
|
+
["analytics user", [
|
|
4675
|
+
"Usage:",
|
|
4676
|
+
" 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>]",
|
|
4677
|
+
"",
|
|
4678
|
+
"Alias for `audienti analytics users`."
|
|
4679
|
+
].join("\n")],
|
|
4680
|
+
|
|
3631
4681
|
["analytics visibility", [
|
|
3632
4682
|
"Usage:",
|
|
3633
4683
|
" audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]",
|
|
@@ -3703,6 +4753,7 @@ const HELP_TOPICS = new Map([
|
|
|
3703
4753
|
" audienti prospects report-bad-profile <prsp_id> <prof_id>",
|
|
3704
4754
|
" audienti prospects add-note <prsp_id> --type steer --message \"Meeting will not happen\" --engagement-type action.meeting.canceled",
|
|
3705
4755
|
" audienti prospects sequence-preview <prsp_id>",
|
|
4756
|
+
" audienti writer test-run <prsp_id>",
|
|
3706
4757
|
" audienti prospects sequence-export <prsp_id> --csv",
|
|
3707
4758
|
"",
|
|
3708
4759
|
"5. Attach existing prospects without re-importing",
|
|
@@ -3717,6 +4768,7 @@ const HELP_TOPICS = new Map([
|
|
|
3717
4768
|
"",
|
|
3718
4769
|
"7. Inspect account analytics",
|
|
3719
4770
|
" audienti analytics prospects --window 24h",
|
|
4771
|
+
" audienti analytics users --user me --window 30d",
|
|
3720
4772
|
" audienti analytics visibility --window 24h --user me",
|
|
3721
4773
|
" audienti analytics content --window week",
|
|
3722
4774
|
"",
|