@audienti/cli 0.1.4 → 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/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
 
@@ -26,6 +27,30 @@ const DEFAULT_PROFILE_IDENTIFIERS = [
26
27
  const DELETE_CONFIRMATION_VALUES = new Set(["yes", "true", "y"]);
27
28
  const PROSPECTS_ADD_NOTE_USAGE = "Usage: audienti prospects add-note <prsp_id> (--message <text> [--type <note|steer|voicemail_outreach|video_outreach>] [--engagement-type <key>] | --payload <file.json>) [--json] [--account <acct_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>]";
30
+ const PROSPECTS_ADD_PROFILE_USAGE = "Usage: audienti prospects add-profile <prsp_id> --url <profile_url|email|phone> [--json] [--account <acct_id>]";
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;
29
54
  const SEQUENCE_EXPORT_CSV_COLUMNS = [
30
55
  "prospect_id",
31
56
  "prospect_name",
@@ -54,7 +79,9 @@ const SEQUENCE_EXPORT_CSV_COLUMNS = [
54
79
  export async function run(argv = process.argv.slice(2), deps = {}) {
55
80
  const context = {
56
81
  env: deps.env || process.env,
82
+ cwd: deps.cwd || process.cwd(),
57
83
  fetchImpl: deps.fetch || globalThis.fetch,
84
+ now: deps.now || (() => new Date()),
58
85
  sleep: deps.sleep || sleep,
59
86
  stdout: deps.stdout || process.stdout,
60
87
  stderr: deps.stderr || process.stderr
@@ -104,6 +131,7 @@ async function dispatch(argv, context) {
104
131
  if (normalizedResource === "motions" && action === "list") return motionsList(rest, context, { accountOverride });
105
132
  if (normalizedResource === "motions" && action === "show") return motionsShow(rest, context, { accountOverride });
106
133
  if (normalizedResource === "motions" && action === "status") return motionsStatus(rest, context, { accountOverride });
134
+ if (normalizedResource === "motions" && action === "analytics") return motionsAnalytics(rest, context, { accountOverride });
107
135
  if (normalizedResource === "motions" && action === "prospects") return motionsProspects(rest, context, { accountOverride });
108
136
  if (normalizedResource === "motions" && action === "add-prospects") return motionsAddProspects(rest, context, { accountOverride });
109
137
  if (normalizedResource === "motions" && action === "create") return motionsCreate(rest, context, { accountOverride });
@@ -114,15 +142,19 @@ async function dispatch(argv, context) {
114
142
  if (normalizedResource === "prospects" && action === "write") return prospectsWrite(rest, context, { accountOverride });
115
143
  if (normalizedResource === "prospects" && action === "add-note") return prospectsAddNote(rest, context, { accountOverride });
116
144
  if (normalizedResource === "prospects" && action === "add-steer") return prospectsAddSteer(rest, context, { accountOverride });
145
+ if (normalizedResource === "prospects" && action === "add-profile") return prospectsAddProfile(rest, context, { accountOverride });
146
+ if (normalizedResource === "prospects" && action === "report-bad-profile") return prospectsReportBadProfile(rest, context, { accountOverride });
117
147
  if (normalizedResource === "prospects" && action === "sequence-preview") return prospectsSequencePreview(rest, context, { accountOverride });
118
148
  if (normalizedResource === "prospects" && action === "sequence-export") return prospectsSequenceExport(rest, context, { accountOverride });
119
149
  if (normalizedResource === "prospects" && action === "import") return prospectsImport(rest, context, { accountOverride });
120
150
  if (normalizedResource === "prospects" && action === "import-status") return prospectsImportStatus(rest, context, { accountOverride });
151
+ if (normalizedResource === "writer" && action === "test-run") return writerTestRun(rest, context, { accountOverride });
121
152
  if (normalizedResource === "tools" && action === "get") return toolsGet(rest, context, { accountOverride });
122
153
  if (normalizedResource === "operator" && action === "queue") return operatorQueue(rest, context, { accountOverride });
123
154
  if (normalizedResource === "operator" && action === "next") return operatorNext(rest, context, { accountOverride });
124
155
  if (normalizedResource === "operator" && action === "outcome") return operatorOutcome(rest, context, { accountOverride });
125
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 });
126
158
  if (normalizedResource === "analytics" && ["visibility", "visops"].includes(action)) return analyticsVisibility(rest, context, { accountOverride });
127
159
  if (normalizedResource === "analytics" && action === "content") return analyticsContent(rest, context, { accountOverride });
128
160
 
@@ -168,11 +200,13 @@ function helpTopicFromArgs(args) {
168
200
  function normalizeTopicParts(parts) {
169
201
  if (parts[0] === "plays") return ["motions", ...parts.slice(1)];
170
202
  if (parts[0] === "principals") return ["users", ...parts.slice(1)];
203
+ if (parts[0] === "writers") return ["writer", ...parts.slice(1)];
171
204
  return parts;
172
205
  }
173
206
 
174
207
  function normalizeResource(resource) {
175
208
  if (resource === "principals") return "users";
209
+ if (resource === "writers") return "writer";
176
210
  return resource === "plays" ? "motions" : resource;
177
211
  }
178
212
 
@@ -651,6 +685,23 @@ async function motionsStatus(args, context, { accountOverride } = {}) {
651
685
  renderMotionStatus(status, context);
652
686
  }
653
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
+
654
705
  async function motionsProspects(args, context, { accountOverride } = {}) {
655
706
  const { values, positionals } = parseCommandArgs(args, {
656
707
  ...jsonOptions(),
@@ -861,6 +912,31 @@ async function prospectsAddSteer(args, context, { accountOverride } = {}) {
861
912
  });
862
913
  }
863
914
 
915
+ async function prospectsAddProfile(args, context, { accountOverride } = {}) {
916
+ const { values, positionals } = parseCommandArgs(args, {
917
+ ...jsonOptions(),
918
+ url: { type: "string" }
919
+ });
920
+ if (positionals.length !== 1 || !values.url) throw new CommandError(PROSPECTS_ADD_PROFILE_USAGE);
921
+
922
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
923
+ const response = await client.addProspectProfile(accountId, positionals[0], { url: values.url });
924
+ if (values.json) return writeJson(context.stdout, response);
925
+
926
+ renderProspectProfileMutation(response, context, { action: "Added" });
927
+ }
928
+
929
+ async function prospectsReportBadProfile(args, context, { accountOverride } = {}) {
930
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
931
+ if (positionals.length !== 2) throw new CommandError(PROSPECTS_REPORT_BAD_PROFILE_USAGE);
932
+
933
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
934
+ const response = await client.reportBadProspectProfile(accountId, positionals[0], { profile_id: positionals[1] });
935
+ if (values.json) return writeJson(context.stdout, response);
936
+
937
+ renderProspectProfileMutation(response, context, { action: "Reported" });
938
+ }
939
+
864
940
  async function prospectNoteCommand(args, context, { accountOverride, forcedType, usageText }) {
865
941
  const { values, positionals } = parseCommandArgs(args, {
866
942
  ...jsonOptions(),
@@ -885,13 +961,63 @@ async function prospectNoteCommand(args, context, { accountOverride, forcedType,
885
961
  }
886
962
 
887
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 }) {
888
1014
  const { values, positionals } = parseCommandArgs(args, {
889
1015
  ...jsonOptions(),
890
1016
  "connection-state": { type: "string" }
891
1017
  });
892
1018
 
893
1019
  if (positionals.length !== 1) {
894
- throw new CommandError("Usage: audienti prospects sequence-preview <prsp_id> [--json] [--connection-state <state>] [--account <acct_id>]");
1020
+ throw new CommandError(usageText);
895
1021
  }
896
1022
 
897
1023
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -900,7 +1026,7 @@ async function prospectsSequencePreview(args, context, { accountOverride } = {})
900
1026
  }));
901
1027
  if (values.json) return writeJson(context.stdout, payload);
902
1028
 
903
- renderProspectSequencePreview(payload, context);
1029
+ renderProspectSequencePreview(payload, context, { title });
904
1030
  }
905
1031
 
906
1032
  async function prospectsSequenceExport(args, context, { accountOverride } = {}) {
@@ -909,10 +1035,12 @@ async function prospectsSequenceExport(args, context, { accountOverride } = {})
909
1035
  csv: { type: "boolean" },
910
1036
  branch: { type: "string" },
911
1037
  branches: { type: "string" },
1038
+ "draft-mode": { type: "string" },
1039
+ "target-step": { type: "string" },
912
1040
  "angle-index": { type: "string" }
913
1041
  });
914
1042
  if (positionals.length !== 1) {
915
- 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>]");
916
1044
  }
917
1045
  if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
918
1046
  if (values.branch && values.branches) throw new CommandError("Choose one branch filter: use either --branch or --branches.");
@@ -920,7 +1048,9 @@ async function prospectsSequenceExport(args, context, { accountOverride } = {})
920
1048
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
921
1049
  const payload = await client.prospectSequenceExport(accountId, positionals[0], compactObject({
922
1050
  branches: values.branches || values.branch,
923
- angle_index: values["angle-index"]
1051
+ angle_index: values["angle-index"],
1052
+ draft_mode: values["draft-mode"],
1053
+ target_step: values["target-step"]
924
1054
  }));
925
1055
  if (values.json) return writeJson(context.stdout, payload);
926
1056
  if (values.csv) return writeLine(context.stdout, sequenceExportRowsToCsv(payload?.rows || []));
@@ -928,6 +1058,160 @@ async function prospectsSequenceExport(args, context, { accountOverride } = {})
928
1058
  renderProspectSequenceExport(payload, context);
929
1059
  }
930
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
+
931
1215
  async function prospectsImport(args, context, { accountOverride } = {}) {
932
1216
  const { values, positionals } = parseCommandArgs(args, {
933
1217
  ...jsonOptions(),
@@ -1013,7 +1297,7 @@ async function toolsGet(args, context, { accountOverride } = {}) {
1013
1297
  }
1014
1298
 
1015
1299
  async function operatorQueue(args, context, { accountOverride } = {}) {
1016
- const { values, positionals } = parseCommandArgs(args, operatorOptions());
1300
+ const { values, positionals } = parseCommandArgs(args, operatorFilterOptions());
1017
1301
  if (positionals.length > 0) throw new CommandError("Usage: audienti operator queue [--json] [filters] [--account <acct_id>]");
1018
1302
 
1019
1303
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -1024,12 +1308,28 @@ async function operatorQueue(args, context, { accountOverride } = {}) {
1024
1308
  }
1025
1309
 
1026
1310
  async function operatorNext(args, context, { accountOverride } = {}) {
1027
- const { values, positionals } = parseCommandArgs(args, operatorOptions());
1028
- if (positionals.length > 0) throw new CommandError("Usage: audienti operator next [--json|--plan] [filters] [--account <acct_id>]");
1311
+ const { values, positionals } = parseCommandArgs(args, operatorNextOptions());
1312
+ if (positionals.length > 0) throw new CommandError("Usage: audienti operator next [--json|--plan|--done|--skip|--fail|--return] [filters] [--note <text>] [--account <acct_id>]");
1029
1313
  if (values.json && values.plan) throw new CommandError("Choose one output format: use either --json or --plan.");
1314
+ const outcomeStatus = operatorNextOutcomeStatus(values);
1315
+ if (values.plan && outcomeStatus) throw new CommandError("Choose one mode: use either --plan or an outcome flag.");
1316
+ if (!outcomeStatus && (values.note !== undefined || values["occurred-at"] !== undefined)) {
1317
+ throw new CommandError("--note and --occurred-at require an outcome flag: --done, --skip, --fail, or --return.");
1318
+ }
1030
1319
 
1031
1320
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
1032
1321
  const payload = await client.operatorNext(accountId, operatorQuery(values));
1322
+ if (outcomeStatus) {
1323
+ const response = await client.operatorOutcome(accountId, operatorNextOutcomePayload(payload?.next_move, {
1324
+ status: outcomeStatus,
1325
+ note: values.note,
1326
+ occurredAt: values["occurred-at"],
1327
+ filters: payload?.filters
1328
+ }));
1329
+ if (values.json) return writeJson(context.stdout, response);
1330
+
1331
+ return renderOperatorOutcome(response, context);
1332
+ }
1033
1333
  if (values.json) return writeJson(context.stdout, payload);
1034
1334
  if (values.plan) return renderOperatorPlan(payload?.next_move, context);
1035
1335
 
@@ -1057,8 +1357,12 @@ async function operatorOutcome(args, context, { accountOverride } = {}) {
1057
1357
  }
1058
1358
 
1059
1359
  async function analyticsProspects(args, context, { accountOverride } = {}) {
1060
- const { values, positionals } = parseCommandArgs(args, analyticsOptions());
1061
- if (positionals.length > 0) throw new CommandError("Usage: audienti analytics prospects [--window 24h] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]");
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);
1062
1366
 
1063
1367
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
1064
1368
  const payload = await client.analyticsProspects(accountId, analyticsQuery(values));
@@ -1067,6 +1371,61 @@ async function analyticsProspects(args, context, { accountOverride } = {}) {
1067
1371
  renderAnalyticsProspects(payload, context);
1068
1372
  }
1069
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
+
1070
1429
  async function analyticsVisibility(args, context, { accountOverride } = {}) {
1071
1430
  const { values, positionals } = parseCommandArgs(args, analyticsOptions());
1072
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>]");
@@ -1144,19 +1503,31 @@ function jsonOptions() {
1144
1503
  };
1145
1504
  }
1146
1505
 
1147
- function operatorOptions() {
1506
+ function operatorFilterOptions(extra = {}) {
1148
1507
  return {
1149
1508
  ...jsonOptions(),
1150
- plan: { type: "boolean" },
1151
1509
  principal: { type: "string" },
1152
1510
  motion: { type: "string" },
1153
1511
  list: { type: "string" },
1154
1512
  stage: { type: "string" },
1155
1513
  "opportunity-kind": { type: "string" },
1156
- "writing-status": { type: "string" }
1514
+ "writing-status": { type: "string" },
1515
+ ...extra
1157
1516
  };
1158
1517
  }
1159
1518
 
1519
+ function operatorNextOptions() {
1520
+ return operatorFilterOptions({
1521
+ plan: { type: "boolean" },
1522
+ done: { type: "boolean" },
1523
+ skip: { type: "boolean" },
1524
+ fail: { type: "boolean" },
1525
+ return: { type: "boolean" },
1526
+ note: { type: "string" },
1527
+ "occurred-at": { type: "string" }
1528
+ });
1529
+ }
1530
+
1160
1531
  function operatorQuery(values) {
1161
1532
  return compactObject({
1162
1533
  principal_account_user_id: values.principal,
@@ -1168,6 +1539,33 @@ function operatorQuery(values) {
1168
1539
  });
1169
1540
  }
1170
1541
 
1542
+ function operatorNextOutcomeStatus(values) {
1543
+ const selected = [
1544
+ values.done ? "done" : null,
1545
+ values.skip ? "skipped" : null,
1546
+ values.fail ? "failed" : null,
1547
+ values.return ? "returned" : null
1548
+ ].filter(Boolean);
1549
+ if (selected.length > 1) throw new CommandError("Choose one outcome flag: --done, --skip, --fail, or --return.");
1550
+
1551
+ return selected[0];
1552
+ }
1553
+
1554
+ function operatorNextOutcomePayload(row, { status, note, occurredAt, filters }) {
1555
+ if (!row) throw new CommandError("No operator moves found.");
1556
+ if (!row.id) throw new CommandError("The next operator move is missing a row id.");
1557
+ if (!row.fingerprint) throw new CommandError("The next operator move is missing a fingerprint; update the server before using outcome shortcuts.");
1558
+
1559
+ return compactObject({
1560
+ row_id: row.id,
1561
+ status,
1562
+ fingerprint: row.fingerprint,
1563
+ queue_filters: filters,
1564
+ note,
1565
+ occurred_at: occurredAt
1566
+ });
1567
+ }
1568
+
1171
1569
  function analyticsOptions() {
1172
1570
  return {
1173
1571
  ...jsonOptions(),
@@ -1176,13 +1574,161 @@ function analyticsOptions() {
1176
1574
  };
1177
1575
  }
1178
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
+
1179
1603
  function analyticsQuery(values) {
1180
1604
  return compactObject({
1181
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,
1182
1610
  account_user_id: values.user
1183
1611
  });
1184
1612
  }
1185
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
+
1186
1732
  function compactObject(object) {
1187
1733
  return Object.fromEntries(
1188
1734
  Object.entries(object).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== "")
@@ -1501,10 +2047,16 @@ function renderBulkMutationResult(payload, context, { successLabel, zeroSuccessL
1501
2047
  function renderMotions(motions, context) {
1502
2048
  if (!Array.isArray(motions) || motions.length === 0) return writeLine(context.stdout, "No motions found.");
1503
2049
 
1504
- writeLine(context.stdout, "MOTION ID\tSTATUS\tKIND\tNAME");
1505
- for (const motion of motions) {
1506
- writeLine(context.stdout, `${display(motion.prefix_id)}\t${display(motion.status)}\t${display(motion.kind)}\t${display(motion.name)}`);
1507
- }
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
+ ];
1508
2060
  }
1509
2061
 
1510
2062
  function renderMotion(motion, context) {
@@ -1676,7 +2228,20 @@ function renderProspectNote(payload, context) {
1676
2228
  }
1677
2229
  }
1678
2230
 
1679
- function renderProspectSequencePreview(payload, context) {
2231
+ function renderProspectProfileMutation(payload, context, { action }) {
2232
+ const prospect = payload?.prospect || {};
2233
+ const profile = payload?.profile || {};
2234
+ const status = payload?.status ? ` (${payload.status})` : "";
2235
+
2236
+ writeLine(context.stdout, `${action} profile${status}.`);
2237
+ writeLine(context.stdout, `Prospect: ${display(prospect.display_name || prospect.name)} (${display(prospect.prefix_id)})`);
2238
+ writeLine(context.stdout, `Profile: ${display(profile.citation_id || profile.prefix_id)}`);
2239
+ if (profile.identifier) writeLine(context.stdout, `Type: ${profile.identifier}`);
2240
+ if (profile.username) writeLine(context.stdout, `Username: ${profile.username}`);
2241
+ if (profile.url) writeLine(context.stdout, `URL: ${profile.url}`);
2242
+ }
2243
+
2244
+ function renderProspectSequencePreview(payload, context, { title = "Sequence preview" } = {}) {
1680
2245
  const prospect = payload?.prospect || {};
1681
2246
  const report = payload?.report || {};
1682
2247
  const preview = report?.last_preview || {};
@@ -1685,6 +2250,7 @@ function renderProspectSequencePreview(payload, context) {
1685
2250
  const steps = Array.isArray(report?.steps) ? report.steps : [];
1686
2251
  const contextInfo = payload?.context || {};
1687
2252
 
2253
+ writeLine(context.stdout, title);
1688
2254
  writeLine(context.stdout, `Prospect: ${display(prospect.display_name || selected.prospect_name)} (${display(prospect.prefix_id || selected.prospect_id)})`);
1689
2255
  if (contextInfo.source) writeLine(context.stdout, `Context: ${contextInfo.source}`);
1690
2256
  if (contextInfo.message) writeLine(context.stdout, contextInfo.message);
@@ -1705,21 +2271,176 @@ function renderProspectSequencePreview(payload, context) {
1705
2271
  writeLine(context.stdout, "");
1706
2272
  writeLine(context.stdout, "Sequence:");
1707
2273
 
1708
- steps.forEach((step, index) => {
1709
- const kind = display(step.kind).toUpperCase();
1710
- const stage = display(step.stage);
1711
- const channel = display(step.channel);
1712
- const timing = step?.timing?.mode === "scheduled" ? ` [scheduled ${display(step?.timing?.scheduled_for)}]` : "";
1713
- writeLine(context.stdout, `${index + 1}. ${kind} | ${stage} | ${channel}${timing}`);
1714
-
1715
- if (step.disposition) writeLine(context.stdout, ` Disposition: ${step.disposition}`);
1716
- if (step.transition_label) writeLine(context.stdout, ` Transition: ${step.transition_label}`);
1717
- if (step.rationale) writeLine(context.stdout, ` Why: ${step.rationale}`);
1718
- if (step.guidance) writeLine(context.stdout, ` Guidance: ${step.guidance}`);
1719
- if (step.body) writeLine(context.stdout, ` Body: ${step.body}`);
1720
- if (step.empty_body_reason) writeLine(context.stdout, ` Empty body reason: ${step.empty_body_reason}`);
1721
- if (step.missing_reason) writeLine(context.stdout, ` Missing reason: ${step.missing_reason}`);
1722
- });
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}`);
1723
2444
  }
1724
2445
 
1725
2446
  function renderProspectSequenceExport(payload, context) {
@@ -1802,17 +2523,13 @@ function renderOperatorQueue(payload, context) {
1802
2523
  return;
1803
2524
  }
1804
2525
 
1805
- writeLine(context.stdout, "MOVE ID\tKIND\tPROSPECT\tMOTION\tNEXT ACTION");
1806
- for (const row of queue) {
1807
- writeLine(context.stdout, operatorRowLine(row));
1808
- }
2526
+ writeOperatorRows(context, queue);
1809
2527
  }
1810
2528
 
1811
2529
  function renderOperatorNext(row, context) {
1812
2530
  if (!row) return writeLine(context.stdout, "No operator moves found.");
1813
2531
 
1814
- writeLine(context.stdout, "MOVE ID\tKIND\tPROSPECT\tMOTION\tNEXT ACTION");
1815
- writeLine(context.stdout, operatorRowLine(row));
2532
+ writeOperatorRows(context, [row]);
1816
2533
  }
1817
2534
 
1818
2535
  function renderOperatorPlan(row, context) {
@@ -1884,11 +2601,72 @@ function renderOperatorOutcome(payload, context) {
1884
2601
 
1885
2602
  function renderAnalyticsProspects(payload, context) {
1886
2603
  writeLine(context.stdout, `Prospect analytics (${analyticsWindowLabel(payload)})`);
2604
+ writeAnalyticsCohort(payload, context);
1887
2605
  writeAnalyticsScope(payload, context);
1888
- writeLine(context.stdout, `Prospects added: ${display(payload?.prospects_added_count, 0)}`);
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
+ }
1889
2611
  writeAnalyticsActionSummary(payload?.actions, context, "Actions");
1890
2612
  writeCountTable(context, "Action breakdown", payload?.actions?.breakdown, ["ACTION", "COUNT", "AUTOMATED", "AUTO %"], actionBreakdownRow);
1891
- 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);
1892
2670
  }
1893
2671
 
1894
2672
  function renderAnalyticsVisibility(payload, context) {
@@ -1908,6 +2686,8 @@ function renderAnalyticsContent(payload, context) {
1908
2686
  }
1909
2687
 
1910
2688
  function writeAnalyticsScope(payload, context) {
2689
+ writeAnalyticsMotion(payload, context);
2690
+ writeAnalyticsProvenance(payload, context);
1911
2691
  if (payload?.account_user) {
1912
2692
  writeLine(context.stdout, `User: ${entityLabel(payload.account_user)}`);
1913
2693
  } else {
@@ -1915,6 +2695,113 @@ function writeAnalyticsScope(payload, context) {
1915
2695
  }
1916
2696
  }
1917
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
+
1918
2805
  function writeAnalyticsActionSummary(actions, context, label) {
1919
2806
  const total = display(actions?.total_count, 0);
1920
2807
  const automated = display(actions?.automated_count, 0);
@@ -1928,8 +2815,32 @@ function writeCountTable(context, title, rows, headers, mapRow) {
1928
2815
  writeLine(context.stdout, title);
1929
2816
  if (list.length === 0) return writeLine(context.stdout, "None");
1930
2817
 
1931
- writeLine(context.stdout, headers.join("\t"));
1932
- for (const row of list) writeLine(context.stdout, mapRow(row).join("\t"));
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;
1933
2844
  }
1934
2845
 
1935
2846
  function actionBreakdownRow(row) {
@@ -1948,6 +2859,37 @@ function countRow(row) {
1948
2859
  ];
1949
2860
  }
1950
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
+
1951
2893
  function analyticsWindowLabel(payload) {
1952
2894
  const window = payload?.window || {};
1953
2895
  const key = display(window.key, "24h");
@@ -1956,18 +2898,67 @@ function analyticsWindowLabel(payload) {
1956
2898
  return `${key}: ${window.started_at} to ${window.ended_at}`;
1957
2899
  }
1958
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
+
1959
2910
  function percentageLabel(value) {
1960
2911
  return value === undefined || value === null || value === "" ? "n/a" : `${value}%`;
1961
2912
  }
1962
2913
 
1963
- function operatorRowLine(row) {
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) {
1964
2926
  return [
1965
2927
  display(row?.id),
1966
- display(row?.opportunity_kind),
1967
- display(row?.prospect?.display_name || row?.prospect?.name || row?.profile?.display_name),
1968
- display(row?.motion?.name),
2928
+ operatorWorkTypeLabel(row),
2929
+ operatorSubjectLabel(row),
2930
+ operatorMotionLabel(row),
1969
2931
  display(nextActionLabel(row))
1970
- ].join("\t");
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}` : "");
1971
2962
  }
1972
2963
 
1973
2964
  function nextActionLabel(source) {
@@ -2232,63 +3223,77 @@ const HELP_TOPICS = new Map([
2232
3223
  "Usage:",
2233
3224
  " audienti <command> [options]",
2234
3225
  "",
2235
- "Start here for local agents:",
2236
- " audienti help agent-workflows",
2237
- "",
2238
- "Implemented commands:",
2239
- " audienti auth token <token> [--host <url>]",
2240
- " audienti auth status",
2241
- " audienti auth logout",
2242
- " audienti config list [--json]",
2243
- " audienti accounts list [--json]",
2244
- " audienti accounts select <acct_id>",
2245
- " audienti users list [--json]",
2246
- " audienti offers list [--json]",
2247
- " audienti offers create --name <text> [--json]",
2248
- " audienti icps list [--json]",
2249
- " audienti icps create (--name <text> | --payload <file.json>) [--json]",
2250
- " audienti companies search --query <text> [--json]",
2251
- " audienti lists list [--json]",
2252
- " audienti lists create --name <text> [--json]",
2253
- " audienti lists show <list_id> [--json]",
2254
- " audienti lists update <list_id> [--json]",
2255
- " audienti lists delete <list_id> --confirm <yes|true|Y|y> [--json]",
2256
- " audienti lists prospects <list_id> [--json]",
2257
- " audienti lists add-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
2258
- " audienti lists remove-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
2259
- " audienti motions list [--json]",
2260
- " audienti motions show <motn_id> [--json]",
2261
- " audienti motions status <motn_id> [--json]",
2262
- " audienti motions prospects <motn_id> [--json]",
2263
- " audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
2264
- " audienti motions create --payload <file.json> [--json]",
2265
- " audienti prospects list [--json]",
2266
- " audienti prospects show <prsp_id> [--json]",
2267
- " audienti prospects timeline <prsp_id> [--json]",
2268
- " audienti prospects message-types <prsp_id> [--json]",
2269
- " audienti prospects write <prsp_id> --type <surface_key> [--json]",
2270
- " audienti prospects add-note <prsp_id> --message <text> [--json]",
2271
- " audienti prospects add-steer <prsp_id> --message <text> [--json]",
2272
- " audienti prospects sequence-preview <prsp_id> [--json]",
2273
- " audienti prospects sequence-export <prsp_id> [--csv]",
2274
- " audienti prospects import <linkedin_url> [--list <list_id>] [--motion <motn_id>] [--json]",
2275
- " audienti prospects import-status <primp_id> [--json]",
2276
- " audienti tools get <email|phone> --url <linkedin_url> [--json]",
2277
- " audienti operator next [--json|--plan]",
2278
- " audienti operator queue [--json]",
2279
- " audienti operator outcome <row_id> --payload <file.json> [--json]",
2280
- " audienti analytics prospects [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
2281
- " audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
2282
- " audienti analytics content [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
2283
- "",
2284
- "Planned submit-shape help topics:",
2285
- " audienti prospects disposition help",
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",
2286
3288
  "",
2287
3289
  "Global options:",
2288
3290
  " --account <acct_id> Use an account for one command without saving it",
2289
3291
  " --help, -h Show help",
2290
3292
  "",
2291
- "Run `audienti <command> help` for accepted options, examples, and payload shapes."
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."
2292
3297
  ].join("\n")],
2293
3298
 
2294
3299
  ["auth", [
@@ -2804,6 +3809,7 @@ const HELP_TOPICS = new Map([
2804
3809
  " audienti motions list [--json]",
2805
3810
  " audienti motions show <motn_id> [--json]",
2806
3811
  " audienti motions status <motn_id> [--json]",
3812
+ " audienti motions analytics <motn_id> [--window 30d] [--json]",
2807
3813
  " audienti motions prospects <motn_id> [--json]",
2808
3814
  " audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
2809
3815
  " audienti motions create --payload <file.json> [--json]",
@@ -2872,6 +3878,27 @@ const HELP_TOPICS = new Map([
2872
3878
  " GET /api/v1/accounts/:account_id/motions/:id/status.json"
2873
3879
  ].join("\n")],
2874
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
+
2875
3902
  ["motions prospects", [
2876
3903
  "Usage:",
2877
3904
  " audienti motions prospects <motn_id> [--json] [--account <acct_id>]",
@@ -2970,6 +3997,8 @@ const HELP_TOPICS = new Map([
2970
3997
  " audienti prospects write <prsp_id> --type <surface_key> [--json]",
2971
3998
  " audienti prospects add-note <prsp_id> --message <text> [--json]",
2972
3999
  " audienti prospects add-steer <prsp_id> --message <text> [--json]",
4000
+ " audienti prospects add-profile <prsp_id> --url <profile_url|email|phone> [--json]",
4001
+ " audienti prospects report-bad-profile <prsp_id> <prof_id|citation_id> [--json]",
2973
4002
  " audienti prospects sequence-preview <prsp_id> [--json]",
2974
4003
  " audienti prospects sequence-export <prsp_id> [--csv]",
2975
4004
  " audienti prospects import <linkedin_url> [--list <list_id>] [--motion <motn_id>] [--json]",
@@ -3181,6 +4210,60 @@ const HELP_TOPICS = new Map([
3181
4210
  " }"
3182
4211
  ].join("\n")],
3183
4212
 
4213
+ ["prospects add-profile", [
4214
+ "Usage:",
4215
+ ` ${PROSPECTS_ADD_PROFILE_USAGE.slice("Usage: ".length)}`,
4216
+ "",
4217
+ "Status: implemented",
4218
+ "",
4219
+ "Purpose:",
4220
+ " Add a profile, email address, or phone number to an existing prospect through the same add-profile path used by the prospect show page.",
4221
+ "",
4222
+ "Input shape:",
4223
+ " prsp_id: prsp_ prospect prefix id",
4224
+ " url: supported profile URL, plain email address, mailto: URL, plain phone number, or tel: URL",
4225
+ "",
4226
+ "Output shape:",
4227
+ " prospect: prospect summary",
4228
+ " profile: attached profile with prefix_id, citation_id, identifier, username, url, and status",
4229
+ " status: attached | already_attached",
4230
+ "",
4231
+ "API:",
4232
+ " POST /api/v1/accounts/:account_id/prospects/:id/profiles.json",
4233
+ "",
4234
+ "JSON body:",
4235
+ " {",
4236
+ " \"url\": \"prospect@example.com\"",
4237
+ " }"
4238
+ ].join("\n")],
4239
+
4240
+ ["prospects report-bad-profile", [
4241
+ "Usage:",
4242
+ ` ${PROSPECTS_REPORT_BAD_PROFILE_USAGE.slice("Usage: ".length)}`,
4243
+ "",
4244
+ "Status: implemented",
4245
+ "",
4246
+ "Purpose:",
4247
+ " Report one of a prospect's attached profiles as bad through the same report action used by the prospect show page.",
4248
+ "",
4249
+ "Input shape:",
4250
+ " prsp_id: prsp_ prospect prefix id",
4251
+ " prof_id: prof_ prefix id or citation id such as email/profile:name@example.com",
4252
+ "",
4253
+ "Output shape:",
4254
+ " prospect: prospect summary",
4255
+ " profile: reported profile",
4256
+ " status: reported",
4257
+ "",
4258
+ "API:",
4259
+ " POST /api/v1/accounts/:account_id/prospects/:id/report_bad_profile.json",
4260
+ "",
4261
+ "JSON body:",
4262
+ " {",
4263
+ " \"profile_id\": \"prof_abc123\"",
4264
+ " }"
4265
+ ].join("\n")],
4266
+
3184
4267
  ["prospects sequence-preview", [
3185
4268
  "Usage:",
3186
4269
  " audienti prospects sequence-preview <prsp_id> [--json] [--connection-state <state>] [--account <acct_id>]",
@@ -3203,9 +4286,54 @@ const HELP_TOPICS = new Map([
3203
4286
  " POST /api/v1/accounts/:account_id/prospects/:id/sequence_preview.json"
3204
4287
  ].join("\n")],
3205
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
+
3206
4334
  ["prospects sequence-export", [
3207
4335
  "Usage:",
3208
- " 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>]",
3209
4337
  "",
3210
4338
  "Status: implemented",
3211
4339
  "",
@@ -3217,6 +4345,11 @@ const HELP_TOPICS = new Map([
3217
4345
  " no-accept: no reply and no accepted connection request",
3218
4346
  " accepted: connection request accepted, then no reply otherwise",
3219
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
+ "",
3220
4353
  "Output shape:",
3221
4354
  " rows[].prospect_id: prsp_",
3222
4355
  " rows[].branch: no_accept | accepted",
@@ -3345,11 +4478,11 @@ const HELP_TOPICS = new Map([
3345
4478
 
3346
4479
  ["operator", [
3347
4480
  "Usage:",
3348
- " audienti operator next [--json|--plan]",
4481
+ " audienti operator next [--json|--plan|--done|--skip|--fail|--return]",
3349
4482
  " audienti operator queue [--json]",
3350
4483
  " audienti operator outcome <row_id> --payload <file.json>",
3351
4484
  "",
3352
- "Status: read commands and prospect outcome writeback implemented",
4485
+ "Status: read commands and prospect next-move writeback implemented",
3353
4486
  "",
3354
4487
  "Filters:",
3355
4488
  " --principal <account_user_id>",
@@ -3362,12 +4495,18 @@ const HELP_TOPICS = new Map([
3362
4495
 
3363
4496
  ["operator next", [
3364
4497
  "Usage:",
3365
- " audienti operator next [--json|--plan] [filters] [--account <acct_id>]",
4498
+ " audienti operator next [--json|--plan|--done|--skip|--fail|--return] [filters] [--note <text>] [--account <acct_id>]",
3366
4499
  "",
3367
4500
  "Status: implemented",
3368
4501
  "",
3369
4502
  "Options:",
3370
4503
  " --plan Render a deterministic static plan from the existing next-action coach payload, CTA, and operator draft state",
4504
+ " --done Mark the current next prospect move completed through the operator outcome API",
4505
+ " --skip Mark the current next prospect move skipped through the operator outcome API",
4506
+ " --fail Mark the current next prospect move failed through the operator outcome API",
4507
+ " --return Mark the current next prospect move returned through the operator outcome API",
4508
+ " --note <text> Optional outcome note used with --done, --skip, --fail, or --return",
4509
+ " --occurred-at <ISO8601> Optional completion timestamp used with an outcome flag",
3371
4510
  "",
3372
4511
  "Output shape:",
3373
4512
  " next_move.id: row id",
@@ -3380,7 +4519,8 @@ const HELP_TOPICS = new Map([
3380
4519
  " metrics: queue-builder metrics",
3381
4520
  "",
3382
4521
  "API:",
3383
- " GET /api/v1/accounts/:account_id/operator/next.json"
4522
+ " GET /api/v1/accounts/:account_id/operator/next.json",
4523
+ " POST /api/v1/accounts/:account_id/operator/outcome.json when an outcome flag is used"
3384
4524
  ].join("\n")],
3385
4525
 
3386
4526
  ["operator queue", [
@@ -3428,7 +4568,9 @@ const HELP_TOPICS = new Map([
3428
4568
 
3429
4569
  ["analytics", [
3430
4570
  "Usage:",
3431
- " 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]",
3432
4574
  " audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
3433
4575
  " audienti analytics visops [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
3434
4576
  " audienti analytics content [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
@@ -3437,23 +4579,56 @@ const HELP_TOPICS = new Map([
3437
4579
  "",
3438
4580
  "Window:",
3439
4581
  " --window <24h|7d|1w|day|week>",
3440
- " --user <account_user_id|email|name|me> Narrow analytics to one account user. Email/name partials are accepted when they match exactly one account user.",
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.",
3441
4589
  "",
3442
4590
  "Output:",
3443
- " Account-scoped analytics for prospects, visibility engagement, and ContentOps publishing."
4591
+ " Account-scoped analytics for prospects, users, visibility engagement, and ContentOps publishing."
3444
4592
  ].join("\n")],
3445
4593
 
3446
4594
  ["analytics prospects", [
3447
4595
  "Usage:",
3448
- " 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>]",
3449
4598
  "",
3450
4599
  "Status: implemented",
3451
4600
  "",
3452
4601
  "Output shape:",
3453
- " prospects_added_count: account prospects added in the window",
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",
3454
4608
  " account_user: selected account user when --user is provided, otherwise null",
3455
- " actions: outbound action totals, type breakdown, and automated percentage",
3456
- " queue_stages[]: current account prospect stage counts",
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.",
3457
4632
  "",
3458
4633
  "API:",
3459
4634
  " GET /api/v1/accounts/:account_id/analytics/prospects.json"
@@ -3466,6 +4641,43 @@ const HELP_TOPICS = new Map([
3466
4641
  "Alias for `audienti analytics prospects`."
3467
4642
  ].join("\n")],
3468
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
+
3469
4681
  ["analytics visibility", [
3470
4682
  "Usage:",
3471
4683
  " audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]",
@@ -3537,8 +4749,11 @@ const HELP_TOPICS = new Map([
3537
4749
  " audienti prospects show <prsp_id>",
3538
4750
  " audienti prospects timeline <prsp_id> --types post,comment,reaction --json",
3539
4751
  " audienti prospects message-types <prsp_id>",
4752
+ " audienti prospects add-profile <prsp_id> --url prospect@example.com",
4753
+ " audienti prospects report-bad-profile <prsp_id> <prof_id>",
3540
4754
  " audienti prospects add-note <prsp_id> --type steer --message \"Meeting will not happen\" --engagement-type action.meeting.canceled",
3541
4755
  " audienti prospects sequence-preview <prsp_id>",
4756
+ " audienti writer test-run <prsp_id>",
3542
4757
  " audienti prospects sequence-export <prsp_id> --csv",
3543
4758
  "",
3544
4759
  "5. Attach existing prospects without re-importing",
@@ -3553,6 +4768,7 @@ const HELP_TOPICS = new Map([
3553
4768
  "",
3554
4769
  "7. Inspect account analytics",
3555
4770
  " audienti analytics prospects --window 24h",
4771
+ " audienti analytics users --user me --window 30d",
3556
4772
  " audienti analytics visibility --window 24h --user me",
3557
4773
  " audienti analytics content --window week",
3558
4774
  "",