@audienti/cli 0.1.11 → 0.1.17

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,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { basename, dirname, join } from "node:path";
4
4
  import { ApiError, AudientiClient, DEFAULT_HOST, normalizeHost } from "./api-client.js";
5
5
  import { configPath, deleteConfig, maskToken, readConfig, writeConfig } from "./config.js";
6
6
 
@@ -17,6 +17,8 @@ const DEFAULT_LIST_LIMIT = 20;
17
17
  const API_MAX_LIST_LIMIT = 100;
18
18
  const DEFAULT_LOOKUP_TIMEOUT_SECONDS = 60;
19
19
  const DEFAULT_LOOKUP_POLL_INTERVAL_SECONDS = 2;
20
+ const PACKAGE_NAME = "@audienti/cli";
21
+ const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
20
22
  const DEFAULT_PROFILE_IDENTIFIERS = [
21
23
  "linkedin/profile",
22
24
  "linkedin/company",
@@ -26,22 +28,50 @@ const DEFAULT_PROFILE_IDENTIFIERS = [
26
28
  ];
27
29
  const DELETE_CONFIRMATION_VALUES = new Set(["yes", "true", "y"]);
28
30
  const MOTION_STATUS_VALUES = new Set(["draft", "preparing", "active", "paused", "archived"]);
31
+ const PROSPECT_STATUS_VALUES = new Set(["active", "nurture", "non_responsive", "not_fit", "rejected"]);
29
32
  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>]";
30
33
  const PROSPECTS_ADD_STEER_USAGE = "Usage: audienti prospects add-steer <prsp_id> (--message <text> [--engagement-type <key>] | --payload <file.json>) [--json] [--account <acct_id>]";
31
34
  const PROSPECTS_ADD_PROFILE_USAGE = "Usage: audienti prospects add-profile <prsp_id> --url <profile_url|email|phone> [--json] [--account <acct_id>]";
32
35
  const PROSPECTS_REPORT_BAD_PROFILE_USAGE = "Usage: audienti prospects report-bad-profile <prsp_id> <prof_id|citation_id> [--json] [--account <acct_id>]";
33
36
  const PROSPECTS_ASSIGN_USAGE = "Usage: audienti prospects assign <prsp_id> [prsp_id...] --assigned-user <id|me|unassign> [--json] [--account <acct_id>]";
37
+ const PROSPECTS_SET_STATUS_USAGE = "Usage: audienti prospects set-status <prsp_id> --status <active|nurture|non_responsive|not_fit|rejected> [--json] [--account <acct_id>]";
38
+ const PROSPECTS_REJECT_USAGE = "Usage: audienti prospects reject <prsp_id> [--json] [--account <acct_id>]";
39
+ const PROSPECTS_NURTURE_USAGE = "Usage: audienti prospects nurture <prsp_id> [--reason <nurture|non_responsive|not_fit>] [--json] [--account <acct_id>]";
40
+ const PROSPECTS_RESTORE_USAGE = "Usage: audienti prospects restore <prsp_id> [--json] [--account <acct_id>]";
41
+ const PROSPECTS_LOCK_USAGE = "Usage: audienti prospects lock <prsp_id> [--note <text>] [--kind <protected_relationship|company_policy>] [--json] [--account <acct_id>]";
42
+ const PROSPECTS_UNLOCK_USAGE = "Usage: audienti prospects unlock <prsp_id> [--json] [--account <acct_id>]";
43
+ const PROSPECTS_CHECK_USAGE = "Usage: audienti prospects check [--json|--csv] [filters] [--account <acct_id>]";
34
44
  const PROSPECTS_IMPORT_BATCH_USAGE = "Usage: audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--assigned-user <id|me>] [--json] [--account <acct_id>]";
45
+ const DNC_ADD_USAGE = "Usage: audienti dnc add <email|citation_id|profile_url> [--json] [--account <acct_id>]";
46
+ const DNC_IMPORT_USAGE = "Usage: audienti dnc import --file <txt|csv> [--json] [--account <acct_id>]";
47
+ const DNC_REMOVE_USAGE = "Usage: audienti dnc remove <dnc_entry_id> [--json] [--account <acct_id>]";
48
+ const COMPANY_RULES_CREATE_USAGE = "Usage: audienti company-rules create (--linkedin-url <url> | --domain <domain>) --disposition <monitor|nurture|not_fit|reject> [--name <text>] [--user <account_user_id|email|me>] [--note <text>] [--json] [--account <acct_id>]";
49
+ const COMPANY_RULES_UPDATE_USAGE = "Usage: audienti company-rules update <rule_id> [--linkedin-url <url>] [--domain <domain>] [--disposition <monitor|nurture|not_fit|reject>] [--name <text>] [--user <account_user_id|email|me|none>] [--note <text>] [--json] [--account <acct_id>]";
50
+ const COMPANY_RULES_REMOVE_USAGE = "Usage: audienti company-rules remove <rule_id> [--json] [--account <acct_id>]";
51
+ const COMPANY_RULES_APPLY_USAGE = "Usage: audienti company-rules apply (<rule_id>|--all) [--json] [--account <acct_id>]";
35
52
  const USERS_ACTIVITY_USAGE = "Usage: audienti users activity [account_user_id|me] [--mode <actor|account_usage>] [--window <24h|7d|30d>] [--platform <linkedin|email|gmail>] [--query <text>] [--limit <n>] [--page <n>] [--json] [--account <acct_id>]";
53
+ const OFFERS_SHOW_USAGE = "Usage: audienti offers show <offr_id> [--json] [--account <acct_id>]";
54
+ const OFFERS_UPDATE_USAGE = "Usage: audienti offers update <offr_id> [--name <text>] [--description <text>] [--url <url>] [--json] [--account <acct_id>]";
55
+ const OFFERS_DELETE_USAGE = "Usage: audienti offers delete <offr_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
36
56
  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>]";
37
57
  const MOTIONS_ANALYTICS_USAGE = "Usage: audienti motions analytics <motn_id> [--window 30d] [--json] [--account <acct_id>]";
38
- const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> --status <draft|preparing|active|paused|archived> [--json] [--account <acct_id>]";
58
+ const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> [--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]";
59
+ const MOTIONS_ADD_TAG_USAGE = "Usage: audienti motions add-tag <motn_id> <tag> [--json] [--account <acct_id>]";
60
+ const MOTIONS_REMOVE_TAG_USAGE = "Usage: audienti motions remove-tag <motn_id> <tag> [--json] [--account <acct_id>]";
39
61
  const MOTIONS_DELETE_USAGE = "Usage: audienti motions delete <motn_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
40
62
  const MOTIONS_CLONE_USAGE = "Usage: audienti motions clone <motn_id> --name <text> [--json] [--account <acct_id>]";
41
63
  const MOTIONS_MOVE_PROSPECTS_USAGE = "Usage: audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...] [--json] [--account <acct_id>]";
64
+ const MOTIONS_RUN_DISCOVERY_USAGE = "Usage: audienti motions run-discovery <motn_id> [--target-count <n>] [--json] [--account <acct_id>]";
65
+ const ICPS_SHOW_USAGE = "Usage: audienti icps show <icp_id> [--json] [--account <acct_id>]";
66
+ const ICPS_UPDATE_USAGE = "Usage: audienti icps update <icp_id> [--name <text>] [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]";
67
+ const ICPS_ADD_TAG_USAGE = "Usage: audienti icps add-tag <icp_id> <tag> [--json] [--account <acct_id>]";
68
+ const ICPS_REMOVE_TAG_USAGE = "Usage: audienti icps remove-tag <icp_id> <tag> [--json] [--account <acct_id>]";
69
+ const LISTS_ADD_TAG_USAGE = "Usage: audienti lists add-tag <list_id> <tag> [--json] [--account <acct_id>]";
70
+ const LISTS_REMOVE_TAG_USAGE = "Usage: audienti lists remove-tag <list_id> <tag> [--json] [--account <acct_id>]";
42
71
  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>]";
43
72
  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>]";
44
73
  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>]";
74
+ const ANALYTICS_DASHBOARD_USAGE = "Usage: audienti analytics dashboard [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--offer <offr_id>] [--icp <icp_id>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
45
75
  const COHORT_STAGE_ORDER = [
46
76
  "identified",
47
77
  "pre_connect",
@@ -120,20 +150,41 @@ async function dispatch(argv, context) {
120
150
  if (normalizedResource === "auth" && action === "status") return authStatus(rest, context, { accountOverride });
121
151
  if (normalizedResource === "auth" && action === "logout") return authLogout(rest, context);
122
152
  if (normalizedResource === "config" && action === "list") return configList(rest, context);
153
+ if (normalizedResource === "update" && action === "check") return updateCheck(rest, context);
123
154
  if (normalizedResource === "accounts" && action === "list") return accountsList(rest, context, { accountOverride });
124
155
  if (normalizedResource === "accounts" && action === "select") return accountsSelect(rest, context);
125
156
  if (normalizedResource === "users" && action === "list") return usersList(rest, context, { accountOverride });
126
157
  if (normalizedResource === "users" && action === "select") return usersSelect(rest, context, { accountOverride });
127
158
  if (normalizedResource === "users" && action === "activity") return usersActivity(rest, context, { accountOverride });
128
159
  if (normalizedResource === "offers" && action === "list") return offersList(rest, context, { accountOverride });
160
+ if (normalizedResource === "offers" && action === "show") return offersShow(rest, context, { accountOverride });
129
161
  if (normalizedResource === "offers" && action === "create") return offersCreate(rest, context, { accountOverride });
162
+ if (normalizedResource === "offers" && action === "update") return offersUpdate(rest, context, { accountOverride });
163
+ if (normalizedResource === "offers" && action === "delete") return offersDelete(rest, context, { accountOverride });
130
164
  if (normalizedResource === "icps" && action === "list") return icpsList(rest, context, { accountOverride });
165
+ if (normalizedResource === "icps" && action === "show") return icpsShow(rest, context, { accountOverride });
131
166
  if (normalizedResource === "icps" && action === "create") return icpsCreate(rest, context, { accountOverride });
167
+ if (normalizedResource === "icps" && action === "update") return icpsUpdate(rest, context, { accountOverride });
168
+ if (normalizedResource === "icps" && action === "add-tag") return icpsTagMutation("add", rest, context, { accountOverride });
169
+ if (normalizedResource === "icps" && action === "remove-tag") return icpsTagMutation("remove", rest, context, { accountOverride });
132
170
  if (normalizedResource === "companies" && action === "search") return companiesSearch(rest, context, { accountOverride });
171
+ if (normalizedResource === "dnc" && action === "list") return dncList(rest, context, { accountOverride });
172
+ if (normalizedResource === "dnc" && action === "add") return dncAdd(rest, context, { accountOverride });
173
+ if (normalizedResource === "dnc" && action === "import") return dncImport(rest, context, { accountOverride });
174
+ if (normalizedResource === "dnc" && ["remove", "delete"].includes(action)) return dncRemove(rest, context, { accountOverride });
175
+ if (normalizedResource === "company-rules" && action === "list") return companyRulesList(rest, context, { accountOverride });
176
+ if (normalizedResource === "company-rules" && action === "create") return companyRulesCreate(rest, context, { accountOverride });
177
+ if (normalizedResource === "company-rules" && action === "update") return companyRulesUpdate(rest, context, { accountOverride });
178
+ if (normalizedResource === "company-rules" && ["remove", "delete"].includes(action)) return companyRulesRemove(rest, context, { accountOverride });
179
+ if (normalizedResource === "company-rules" && action === "apply") return companyRulesApply(rest, context, { accountOverride });
180
+ if (normalizedResource === "tags" && action === "list") return tagsList(rest, context, { accountOverride });
181
+ if (normalizedResource === "tags" && action === "show") return tagsShow(rest, context, { accountOverride });
133
182
  if (normalizedResource === "lists" && action === "list") return listsList(rest, context, { accountOverride });
134
183
  if (normalizedResource === "lists" && action === "create") return listsCreate(rest, context, { accountOverride });
135
184
  if (normalizedResource === "lists" && action === "show") return listsShow(rest, context, { accountOverride });
136
185
  if (normalizedResource === "lists" && action === "update") return listsUpdate(rest, context, { accountOverride });
186
+ if (normalizedResource === "lists" && action === "add-tag") return listsTagMutation("add", rest, context, { accountOverride });
187
+ if (normalizedResource === "lists" && action === "remove-tag") return listsTagMutation("remove", rest, context, { accountOverride });
137
188
  if (normalizedResource === "lists" && action === "delete") return listsDelete(rest, context, { accountOverride });
138
189
  if (normalizedResource === "lists" && action === "prospects") return listProspects(rest, context, { accountOverride });
139
190
  if (normalizedResource === "lists" && action === "add-prospects") return listsAddProspects(rest, context, { accountOverride });
@@ -146,13 +197,23 @@ async function dispatch(argv, context) {
146
197
  if (normalizedResource === "motions" && action === "add-prospects") return motionsAddProspects(rest, context, { accountOverride });
147
198
  if (normalizedResource === "motions" && action === "create") return motionsCreate(rest, context, { accountOverride });
148
199
  if (normalizedResource === "motions" && action === "update") return motionsUpdate(rest, context, { accountOverride });
200
+ if (normalizedResource === "motions" && action === "add-tag") return motionsTagMutation("add", rest, context, { accountOverride });
201
+ if (normalizedResource === "motions" && action === "remove-tag") return motionsTagMutation("remove", rest, context, { accountOverride });
149
202
  if (normalizedResource === "motions" && ["activate", "pause", "archive"].includes(action)) return motionsStatusShortcut(action, rest, context, { accountOverride });
150
203
  if (normalizedResource === "motions" && action === "delete") return motionsDelete(rest, context, { accountOverride });
151
204
  if (normalizedResource === "motions" && action === "clone") return motionsClone(rest, context, { accountOverride });
152
205
  if (normalizedResource === "motions" && action === "move-prospects") return motionsMoveProspects(rest, context, { accountOverride });
206
+ if (normalizedResource === "motions" && action === "run-discovery") return motionsRunDiscovery(rest, context, { accountOverride });
153
207
  if (normalizedResource === "prospects" && action === "list") return prospectsList(rest, context, { accountOverride });
208
+ if (normalizedResource === "prospects" && action === "check") return prospectsCheck(rest, context, { accountOverride });
154
209
  if (normalizedResource === "prospects" && action === "show") return prospectsShow(rest, context, { accountOverride });
155
210
  if (normalizedResource === "prospects" && action === "assign") return prospectsAssign(rest, context, { accountOverride });
211
+ if (normalizedResource === "prospects" && action === "set-status") return prospectsSetStatus(rest, context, { accountOverride });
212
+ if (normalizedResource === "prospects" && action === "reject") return prospectsDisposition("reject", rest, context, { accountOverride });
213
+ if (normalizedResource === "prospects" && action === "nurture") return prospectsDisposition("nurture", rest, context, { accountOverride });
214
+ if (normalizedResource === "prospects" && action === "restore") return prospectsDisposition("restore", rest, context, { accountOverride });
215
+ if (normalizedResource === "prospects" && action === "lock") return prospectsLock(rest, context, { accountOverride });
216
+ if (normalizedResource === "prospects" && action === "unlock") return prospectsUnlock(rest, context, { accountOverride });
156
217
  if (normalizedResource === "prospects" && action === "timeline") return prospectsTimeline(rest, context, { accountOverride });
157
218
  if (normalizedResource === "prospects" && action === "message-types") return prospectsMessageTypes(rest, context, { accountOverride });
158
219
  if (normalizedResource === "prospects" && action === "write") return prospectsWrite(rest, context, { accountOverride });
@@ -174,6 +235,7 @@ async function dispatch(argv, context) {
174
235
  if (normalizedResource === "analytics" && ["users", "user"].includes(action)) return analyticsUsers(rest, context, { accountOverride });
175
236
  if (normalizedResource === "analytics" && ["visibility", "visops"].includes(action)) return analyticsVisibility(rest, context, { accountOverride });
176
237
  if (normalizedResource === "analytics" && action === "content") return analyticsContent(rest, context, { accountOverride });
238
+ if (normalizedResource === "analytics" && ["dashboard", "campaign", "campaigns"].includes(action)) return analyticsDashboard(rest, context, { accountOverride });
177
239
 
178
240
  throw new CommandError(usage(), { exitCode: resource ? 1 : 0 });
179
241
  }
@@ -224,6 +286,7 @@ function normalizeTopicParts(parts) {
224
286
  function normalizeResource(resource) {
225
287
  if (resource === "principals") return "users";
226
288
  if (resource === "writers") return "writer";
289
+ if (resource === "company_rules" || resource === "company-rules") return "company-rules";
227
290
  return resource === "plays" ? "motions" : resource;
228
291
  }
229
292
 
@@ -332,6 +395,51 @@ async function configList(args, context) {
332
395
  }
333
396
  }
334
397
 
398
+ async function updateCheck(args, context) {
399
+ const { values, positionals } = parseCommandArgs(args, {
400
+ ...jsonOptions(),
401
+ registry: { type: "string" }
402
+ });
403
+ if (positionals.length > 0) throw new CommandError("Usage: audienti update check [--json] [--registry <url>]");
404
+
405
+ const localPackage = await readLocalPackageMetadata();
406
+ let payload;
407
+
408
+ try {
409
+ const latestVersion = await fetchLatestPackageVersion({
410
+ fetchImpl: context.fetchImpl,
411
+ registry: values.registry
412
+ });
413
+ const updateAvailable = compareVersions(localPackage.version, latestVersion) < 0;
414
+ payload = updateCheckPayload({
415
+ currentVersion: localPackage.version,
416
+ latestVersion,
417
+ status: updateAvailable ? "update_available" : "current",
418
+ updateAvailable,
419
+ registry: values.registry || DEFAULT_NPM_REGISTRY,
420
+ now: context.now()
421
+ });
422
+ } catch (error) {
423
+ payload = updateCheckPayload({
424
+ currentVersion: localPackage.version,
425
+ latestVersion: null,
426
+ status: "unknown",
427
+ updateAvailable: null,
428
+ registry: values.registry || DEFAULT_NPM_REGISTRY,
429
+ error: error.message,
430
+ now: context.now()
431
+ });
432
+ }
433
+
434
+ if (values.json) {
435
+ writeJson(context.stdout, payload);
436
+ return payload.status === "unknown" ? 1 : 0;
437
+ }
438
+
439
+ renderUpdateCheck(payload, context);
440
+ return payload.status === "unknown" ? 1 : 0;
441
+ }
442
+
335
443
  async function accountsList(args, context, { accountOverride } = {}) {
336
444
  const { values, positionals } = parseCommandArgs(args, {
337
445
  json: { type: "boolean" }
@@ -452,11 +560,14 @@ function accountUserLabel(accountUser) {
452
560
  }
453
561
 
454
562
  async function listsList(args, context, { accountOverride } = {}) {
455
- const { values, positionals } = parseCommandArgs(args, jsonOptions());
456
- if (positionals.length > 0) throw new CommandError("Usage: audienti lists list [--json] [--account <acct_id>]");
563
+ const { values, positionals } = parseCommandArgs(args, {
564
+ ...jsonOptions(),
565
+ tag: { type: "string" }
566
+ });
567
+ if (positionals.length > 0) throw new CommandError("Usage: audienti lists list [--tag <tag>] [--json] [--account <acct_id>]");
457
568
 
458
569
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
459
- const lists = await client.lists(accountId);
570
+ const lists = filterRecordsByTag(await client.lists(accountId), values.tag, "tags");
460
571
  if (values.json) return writeJson(context.stdout, lists);
461
572
 
462
573
  renderLists(lists, context);
@@ -533,6 +644,17 @@ async function offersList(args, context, { accountOverride } = {}) {
533
644
  renderOffers(offers, context);
534
645
  }
535
646
 
647
+ async function offersShow(args, context, { accountOverride } = {}) {
648
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
649
+ if (positionals.length !== 1) throw new CommandError(OFFERS_SHOW_USAGE);
650
+
651
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
652
+ const offer = await client.offer(accountId, positionals[0]);
653
+ if (values.json) return writeJson(context.stdout, offer);
654
+
655
+ renderOffer(offer, context);
656
+ }
657
+
536
658
  async function offersCreate(args, context, { accountOverride } = {}) {
537
659
  const { values, positionals } = parseCommandArgs(args, {
538
660
  ...jsonOptions(),
@@ -559,27 +681,85 @@ async function offersCreate(args, context, { accountOverride } = {}) {
559
681
  if (offer?.url) writeLine(context.stdout, `URL: ${offer.url}`);
560
682
  }
561
683
 
684
+ async function offersUpdate(args, context, { accountOverride } = {}) {
685
+ const { values, positionals } = parseCommandArgs(args, {
686
+ ...jsonOptions(),
687
+ name: { type: "string" },
688
+ description: { type: "string" },
689
+ url: { type: "string" }
690
+ });
691
+ const hasUpdateField = values.name || values.description !== undefined || values.url !== undefined;
692
+ if (positionals.length !== 1 || !hasUpdateField) {
693
+ throw new CommandError(OFFERS_UPDATE_USAGE);
694
+ }
695
+
696
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
697
+ const offer = await client.updateOffer(accountId, positionals[0], {
698
+ offer: compactObject({
699
+ name: values.name,
700
+ description: values.description,
701
+ url: values.url
702
+ })
703
+ });
704
+ if (values.json) return writeJson(context.stdout, offer);
705
+
706
+ writeLine(context.stdout, `Updated offer ${display(offer?.name)} (${display(offer?.prefix_id)}).`);
707
+ renderOffer(offer, context);
708
+ }
709
+
710
+ async function offersDelete(args, context, { accountOverride } = {}) {
711
+ const { values, positionals } = parseCommandArgs(args, {
712
+ ...jsonOptions(),
713
+ confirm: { type: "string" }
714
+ });
715
+ const normalizedConfirm = String(values.confirm || "").trim().toLowerCase();
716
+ if (positionals.length !== 1 || !DELETE_CONFIRMATION_VALUES.has(normalizedConfirm)) {
717
+ throw new CommandError(OFFERS_DELETE_USAGE);
718
+ }
719
+
720
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
721
+ const payload = await client.deleteOffer(accountId, positionals[0]);
722
+ if (values.json) return writeJson(context.stdout, payload);
723
+
724
+ writeLine(context.stdout, `Deleted offer ${display(payload?.name)} (${display(payload?.prefix_id)}).`);
725
+ }
726
+
562
727
  async function icpsList(args, context, { accountOverride } = {}) {
563
- const { values, positionals } = parseCommandArgs(args, jsonOptions());
564
- if (positionals.length > 0) throw new CommandError("Usage: audienti icps list [--json] [--account <acct_id>]");
728
+ const { values, positionals } = parseCommandArgs(args, {
729
+ ...jsonOptions(),
730
+ tag: { type: "string" }
731
+ });
732
+ if (positionals.length > 0) throw new CommandError("Usage: audienti icps list [--tag <tag>] [--json] [--account <acct_id>]");
565
733
 
566
734
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
567
- const icps = await client.icps(accountId);
735
+ const icps = filterRecordsByTag(await client.icps(accountId), values.tag, "tags");
568
736
  if (values.json) return writeJson(context.stdout, icps);
569
737
 
570
738
  renderIcps(icps, context);
571
739
  }
572
740
 
741
+ async function icpsShow(args, context, { accountOverride } = {}) {
742
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
743
+ if (positionals.length !== 1) throw new CommandError(ICPS_SHOW_USAGE);
744
+
745
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
746
+ const icp = await client.icp(accountId, positionals[0]);
747
+ if (values.json) return writeJson(context.stdout, icp);
748
+
749
+ renderIcp(icp, context);
750
+ }
751
+
573
752
  async function icpsCreate(args, context, { accountOverride } = {}) {
574
753
  const { values, positionals } = parseCommandArgs(args, {
575
754
  ...jsonOptions(),
576
755
  payload: { type: "string" },
577
756
  name: { type: "string" },
578
757
  notes: { type: "string" },
758
+ tags: { type: "string" },
579
759
  "discovery-keyword": { type: "string" }
580
760
  });
581
761
  if (positionals.length > 0) {
582
- throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] | --payload <file.json>) [--json] [--account <acct_id>]");
762
+ throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] | --payload <file.json>) [--json] [--account <acct_id>]");
583
763
  }
584
764
 
585
765
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -589,9 +769,58 @@ async function icpsCreate(args, context, { accountOverride } = {}) {
589
769
 
590
770
  writeLine(context.stdout, `Created ICP ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
591
771
  if (icp?.notes) writeLine(context.stdout, `Notes: ${icp.notes}`);
772
+ if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
592
773
  if (icp?.discovery_keyword) writeLine(context.stdout, `Discovery keyword: ${icp.discovery_keyword}`);
593
774
  }
594
775
 
776
+ async function icpsUpdate(args, context, { accountOverride } = {}) {
777
+ const { values, positionals } = parseCommandArgs(args, {
778
+ ...jsonOptions(),
779
+ name: { type: "string" },
780
+ notes: { type: "string" },
781
+ tags: { type: "string" },
782
+ "discovery-keyword": { type: "string" }
783
+ });
784
+ const hasUpdateField = values.name || values.notes !== undefined || values.tags !== undefined || values["discovery-keyword"] !== undefined;
785
+ if (positionals.length !== 1 || !hasUpdateField) {
786
+ throw new CommandError(ICPS_UPDATE_USAGE);
787
+ }
788
+
789
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
790
+ const icp = await client.updateIcp(accountId, positionals[0], {
791
+ icp: compactObject({
792
+ name: values.name,
793
+ notes: values.notes,
794
+ discovery_keyword: values["discovery-keyword"],
795
+ tags: values.tags !== undefined ? tagList(values.tags) : undefined
796
+ })
797
+ });
798
+ if (values.json) return writeJson(context.stdout, icp);
799
+
800
+ writeLine(context.stdout, `Updated ICP ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
801
+ renderIcp(icp, context);
802
+ }
803
+
804
+ async function icpsTagMutation(action, args, context, { accountOverride } = {}) {
805
+ const usageText = action === "add" ? ICPS_ADD_TAG_USAGE : ICPS_REMOVE_TAG_USAGE;
806
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
807
+ if (positionals.length !== 2) {
808
+ throw new CommandError(usageText);
809
+ }
810
+
811
+ const [icpId, tag] = positionals;
812
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
813
+ const icp = action === "add" ?
814
+ await client.addIcpTag(accountId, icpId, { tag }) :
815
+ await client.removeIcpTag(accountId, icpId, { tag });
816
+ if (values.json) return writeJson(context.stdout, icp);
817
+
818
+ const verb = action === "add" ? "Added" : "Removed";
819
+ const preposition = action === "add" ? "to" : "from";
820
+ writeLine(context.stdout, `${verb} tag ${display(tag)} ${preposition} ICP ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
821
+ if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
822
+ }
823
+
595
824
  async function companiesSearch(args, context, { accountOverride } = {}) {
596
825
  const { values, positionals } = parseCommandArgs(args, {
597
826
  ...jsonOptions(),
@@ -608,16 +837,175 @@ async function companiesSearch(args, context, { accountOverride } = {}) {
608
837
  renderCompanies(payload, context);
609
838
  }
610
839
 
840
+ async function dncList(args, context, { accountOverride } = {}) {
841
+ const { values, positionals } = parseCommandArgs(args, {
842
+ ...jsonOptions(),
843
+ limit: { type: "string" },
844
+ offset: { type: "string" }
845
+ });
846
+ if (positionals.length > 0) throw new CommandError("Usage: audienti dnc list [--limit <n>] [--offset <n>] [--json] [--account <acct_id>]");
847
+
848
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
849
+ const payload = await client.dncEntries(accountId, compactObject({ limit: values.limit, offset: values.offset }));
850
+ if (values.json) return writeJson(context.stdout, payload);
851
+
852
+ renderDncEntries(payload, context);
853
+ }
854
+
855
+ async function dncAdd(args, context, { accountOverride } = {}) {
856
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
857
+ if (positionals.length !== 1) throw new CommandError(DNC_ADD_USAGE);
858
+
859
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
860
+ const payload = await client.createDncEntry(accountId, { value: positionals[0] });
861
+ if (values.json) return writeJson(context.stdout, payload);
862
+
863
+ writeLine(context.stdout, `DNC entry ${display(payload?.status, "created")}: ${display(payload?.dnc_entry?.canonical_value || payload?.dnc_entry?.citation_id)} (${display(payload?.dnc_entry?.prefix_id || payload?.dnc_entry?.id)}).`);
864
+ }
865
+
866
+ async function dncImport(args, context, { accountOverride } = {}) {
867
+ const { values, positionals } = parseCommandArgs(args, {
868
+ ...jsonOptions(),
869
+ file: { type: "string" }
870
+ });
871
+ if (positionals.length > 0 || !values.file) throw new CommandError(DNC_IMPORT_USAGE);
872
+
873
+ const body = await readFile(values.file, "utf8");
874
+ const importValues = parseDncImportValues(body);
875
+ if (importValues.length === 0) throw new CommandError("DNC import file did not contain any values.");
876
+
877
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
878
+ const payload = await client.importDncEntries(accountId, { values: importValues, filename: basename(values.file) });
879
+ if (values.json) return writeJson(context.stdout, payload);
880
+
881
+ writeLine(context.stdout, `Imported DNC entries. Accepted ${display(payload?.accepted_count, 0)}, skipped ${display(payload?.skipped_count, 0)}, invalid ${display(payload?.invalid_count, 0)}, matched ${display(payload?.matched_prospect_count, 0)}.`);
882
+ }
883
+
884
+ async function dncRemove(args, context, { accountOverride } = {}) {
885
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
886
+ if (positionals.length !== 1) throw new CommandError(DNC_REMOVE_USAGE);
887
+
888
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
889
+ const payload = await client.deleteDncEntry(accountId, positionals[0]);
890
+ if (values.json) return writeJson(context.stdout, payload);
891
+
892
+ writeLine(context.stdout, `Removed DNC entry ${display(payload?.prefix_id || payload?.id || positionals[0])}.`);
893
+ }
894
+
895
+ async function companyRulesList(args, context, { accountOverride } = {}) {
896
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
897
+ if (positionals.length > 0) throw new CommandError("Usage: audienti company-rules list [--json] [--account <acct_id>]");
898
+
899
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
900
+ const payload = await client.companyRules(accountId);
901
+ if (values.json) return writeJson(context.stdout, payload);
902
+
903
+ renderCompanyRules(payload, context);
904
+ }
905
+
906
+ async function companyRulesCreate(args, context, { accountOverride } = {}) {
907
+ const { values, positionals } = parseCommandArgs(args, companyRuleOptions());
908
+ if (positionals.length > 0 || (!values["linkedin-url"] && !values.domain) || !values.disposition) {
909
+ throw new CommandError(COMPANY_RULES_CREATE_USAGE);
910
+ }
911
+
912
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
913
+ const payload = await client.createCompanyRule(accountId, { company_rule: companyRulePayload(values) });
914
+ if (values.json) return writeJson(context.stdout, payload);
915
+
916
+ const rule = payload?.company_rule;
917
+ writeLine(context.stdout, `Created company rule ${display(rule?.name || rule?.domain || rule?.linkedin_company_identifier)} (${display(rule?.prefix_id || rule?.id)}).`);
918
+ writeLine(context.stdout, `Disposition: ${display(rule?.disposition)} | Scope: ${companyRuleScopeLabel(rule)}`);
919
+ }
920
+
921
+ async function companyRulesUpdate(args, context, { accountOverride } = {}) {
922
+ const { values, positionals } = parseCommandArgs(args, companyRuleOptions());
923
+ const hasUpdate = values.disposition || values.name !== undefined || values["linkedin-url"] !== undefined || values.domain !== undefined || values.user !== undefined || values.note !== undefined;
924
+ if (positionals.length !== 1 || !hasUpdate) throw new CommandError(COMPANY_RULES_UPDATE_USAGE);
925
+
926
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
927
+ const payload = await client.updateCompanyRule(accountId, positionals[0], { company_rule: companyRulePayload(values) });
928
+ if (values.json) return writeJson(context.stdout, payload);
929
+
930
+ const rule = payload?.company_rule;
931
+ writeLine(context.stdout, `Updated company rule ${display(rule?.name || rule?.domain || rule?.linkedin_company_identifier)} (${display(rule?.prefix_id || rule?.id)}).`);
932
+ writeLine(context.stdout, `Disposition: ${display(rule?.disposition)} | Scope: ${companyRuleScopeLabel(rule)}`);
933
+ }
934
+
935
+ async function companyRulesRemove(args, context, { accountOverride } = {}) {
936
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
937
+ if (positionals.length !== 1) throw new CommandError(COMPANY_RULES_REMOVE_USAGE);
938
+
939
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
940
+ const payload = await client.deleteCompanyRule(accountId, positionals[0]);
941
+ if (values.json) return writeJson(context.stdout, payload);
942
+
943
+ writeLine(context.stdout, `Removed company rule ${display(payload?.company_rule?.prefix_id || payload?.company_rule?.id || positionals[0])}.`);
944
+ }
945
+
946
+ async function companyRulesApply(args, context, { accountOverride } = {}) {
947
+ const { values, positionals } = parseCommandArgs(args, {
948
+ ...jsonOptions(),
949
+ all: { type: "boolean" }
950
+ });
951
+ if ((values.all && positionals.length > 0) || (!values.all && positionals.length !== 1)) {
952
+ throw new CommandError(COMPANY_RULES_APPLY_USAGE);
953
+ }
954
+
955
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
956
+ const payload = values.all ?
957
+ await client.applyAllCompanyRules(accountId) :
958
+ await client.applyCompanyRule(accountId, positionals[0]);
959
+ if (values.json) return writeJson(context.stdout, payload);
960
+
961
+ if (values.all) {
962
+ writeLine(context.stdout, `Applied company rules. Matched ${display(payload?.matched_count, 0)}, changed ${display(payload?.applied_count, 0)}.`);
963
+ } else {
964
+ writeLine(context.stdout, `Applied company rule. Matched ${display(payload?.matched_count, 0)}, changed ${display(payload?.applied_count, 0)}.`);
965
+ }
966
+ }
967
+
968
+ async function tagsList(args, context, { accountOverride } = {}) {
969
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
970
+ if (positionals.length > 0) throw new CommandError("Usage: audienti tags list [--json] [--account <acct_id>]");
971
+
972
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
973
+ const payload = await client.tags(accountId);
974
+ if (values.json) return writeJson(context.stdout, payload);
975
+
976
+ renderTags(payload, context);
977
+ }
978
+
979
+ async function tagsShow(args, context, { accountOverride } = {}) {
980
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
981
+ if (positionals.length !== 1) throw new CommandError("Usage: audienti tags show <tag> [--json] [--account <acct_id>]");
982
+
983
+ const tag = tagList(positionals[0])[0];
984
+ if (!tag) throw new CommandError("Usage: audienti tags show <tag> [--json] [--account <acct_id>]");
985
+
986
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
987
+ const payload = {
988
+ tag,
989
+ icps: filterRecordsByTag(await client.icps(accountId), tag, "tags"),
990
+ lists: filterRecordsByTag(await client.lists(accountId), tag, "tags"),
991
+ motions: filterRecordsByTag(await client.motions(accountId), tag, "play_tags")
992
+ };
993
+ if (values.json) return writeJson(context.stdout, payload);
994
+
995
+ renderTagDetails(payload, context);
996
+ }
997
+
611
998
  async function listsCreate(args, context, { accountOverride } = {}) {
612
999
  const { values, positionals } = parseCommandArgs(args, {
613
1000
  ...jsonOptions(),
614
1001
  name: { type: "string" },
615
1002
  description: { type: "string" },
1003
+ tags: { type: "string" },
616
1004
  "campaign-hook": { type: "string" },
617
1005
  "audience-note": { type: "string" }
618
1006
  });
619
1007
  if (positionals.length > 0 || !values.name) {
620
- throw new CommandError("Usage: audienti lists create --name <text> [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
1008
+ throw new CommandError("Usage: audienti lists create --name <text> [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
621
1009
  }
622
1010
 
623
1011
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -629,6 +1017,7 @@ async function listsCreate(args, context, { accountOverride } = {}) {
629
1017
  list: compactObject({
630
1018
  name: values.name,
631
1019
  description: values.description,
1020
+ tags: tagList(values.tags),
632
1021
  campaign_brief: Object.keys(campaignBrief).length > 0 ? campaignBrief : undefined
633
1022
  })
634
1023
  });
@@ -654,13 +1043,14 @@ async function listsUpdate(args, context, { accountOverride } = {}) {
654
1043
  ...jsonOptions(),
655
1044
  name: { type: "string" },
656
1045
  description: { type: "string" },
1046
+ tags: { type: "string" },
657
1047
  "campaign-hook": { type: "string" },
658
1048
  "audience-note": { type: "string" }
659
1049
  });
660
1050
  const hasCampaignUpdate = values["campaign-hook"] || values["audience-note"];
661
- const hasUpdateField = values.name || values.description || hasCampaignUpdate;
1051
+ const hasUpdateField = values.name || values.description || values.tags !== undefined || hasCampaignUpdate;
662
1052
  if (positionals.length !== 1 || !hasUpdateField) {
663
- throw new CommandError("Usage: audienti lists update <list_id> [--name <text>] [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
1053
+ throw new CommandError("Usage: audienti lists update <list_id> [--name <text>] [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]");
664
1054
  }
665
1055
 
666
1056
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
@@ -672,6 +1062,7 @@ async function listsUpdate(args, context, { accountOverride } = {}) {
672
1062
  list: compactObject({
673
1063
  name: values.name,
674
1064
  description: values.description,
1065
+ tags: values.tags !== undefined ? tagList(values.tags) : undefined,
675
1066
  campaign_brief: Object.keys(campaignBrief).length > 0 ? campaignBrief : undefined
676
1067
  })
677
1068
  });
@@ -681,6 +1072,26 @@ async function listsUpdate(args, context, { accountOverride } = {}) {
681
1072
  if (payload?.description) writeLine(context.stdout, `Description: ${payload.description}`);
682
1073
  }
683
1074
 
1075
+ async function listsTagMutation(action, args, context, { accountOverride } = {}) {
1076
+ const usageText = action === "add" ? LISTS_ADD_TAG_USAGE : LISTS_REMOVE_TAG_USAGE;
1077
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1078
+ if (positionals.length !== 2) {
1079
+ throw new CommandError(usageText);
1080
+ }
1081
+
1082
+ const [listId, tag] = positionals;
1083
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1084
+ const payload = action === "add" ?
1085
+ await client.addListTag(accountId, listId, { tag }) :
1086
+ await client.removeListTag(accountId, listId, { tag });
1087
+ if (values.json) return writeJson(context.stdout, payload);
1088
+
1089
+ const verb = action === "add" ? "Added" : "Removed";
1090
+ const preposition = action === "add" ? "to" : "from";
1091
+ writeLine(context.stdout, `${verb} tag ${display(tag)} ${preposition} list ${display(payload?.name)} (${display(payload?.prefix_id)}).`);
1092
+ if (Array.isArray(payload?.tags)) writeLine(context.stdout, `Tags: ${display(payload.tags.join(", "), "-")}`);
1093
+ }
1094
+
684
1095
  async function listsDelete(args, context, { accountOverride } = {}) {
685
1096
  const { values, positionals } = parseCommandArgs(args, {
686
1097
  ...jsonOptions(),
@@ -780,11 +1191,14 @@ async function listsRemoveProspects(args, context, { accountOverride } = {}) {
780
1191
  }
781
1192
 
782
1193
  async function motionsList(args, context, { accountOverride } = {}) {
783
- const { values, positionals } = parseCommandArgs(args, jsonOptions());
784
- if (positionals.length > 0) throw new CommandError("Usage: audienti motions list [--json] [--account <acct_id>]");
1194
+ const { values, positionals } = parseCommandArgs(args, {
1195
+ ...jsonOptions(),
1196
+ tag: { type: "string" }
1197
+ });
1198
+ if (positionals.length > 0) throw new CommandError("Usage: audienti motions list [--tag <tag>] [--json] [--account <acct_id>]");
785
1199
 
786
1200
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
787
- const motions = await client.motions(accountId);
1201
+ const motions = filterRecordsByTag(await client.motions(accountId), values.tag, "play_tags");
788
1202
  if (values.json) return writeJson(context.stdout, motions);
789
1203
 
790
1204
  renderMotions(motions, context);
@@ -812,6 +1226,22 @@ async function motionsStatus(args, context, { accountOverride } = {}) {
812
1226
  renderMotionStatus(status, context);
813
1227
  }
814
1228
 
1229
+ async function motionsRunDiscovery(args, context, { accountOverride } = {}) {
1230
+ const { values, positionals } = parseCommandArgs(args, {
1231
+ ...jsonOptions(),
1232
+ "target-count": { type: "string" }
1233
+ });
1234
+ if (positionals.length !== 1) throw new CommandError(MOTIONS_RUN_DISCOVERY_USAGE);
1235
+
1236
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1237
+ const payload = await client.runMotionDiscovery(accountId, positionals[0], compactObject({
1238
+ target_count: normalizeOptionalPositiveInteger(values["target-count"], "--target-count")
1239
+ }));
1240
+ if (values.json) return writeJson(context.stdout, payload);
1241
+
1242
+ renderMotionDiscoveryRun(payload, context);
1243
+ }
1244
+
815
1245
  async function motionsAnalytics(args, context, { accountOverride } = {}) {
816
1246
  const { values, positionals } = parseCommandArgs(args, {
817
1247
  ...jsonOptions(),
@@ -929,13 +1359,26 @@ async function motionsDelete(args, context, { accountOverride } = {}) {
929
1359
  async function motionsUpdate(args, context, { accountOverride } = {}) {
930
1360
  const { values, positionals } = parseCommandArgs(args, {
931
1361
  ...jsonOptions(),
932
- status: { type: "string" }
1362
+ status: { type: "string" },
1363
+ tags: { type: "string" }
933
1364
  });
934
- if (positionals.length !== 1 || !values.status) {
1365
+ const hasUpdateField = values.status || values.tags !== undefined;
1366
+ if (positionals.length !== 1 || !hasUpdateField) {
935
1367
  throw new CommandError(MOTIONS_UPDATE_USAGE);
936
1368
  }
937
1369
 
938
- return updateMotionStatus(positionals[0], values.status, context, { accountOverride, json: values.json });
1370
+ const normalizedStatus = normalizeMotionStatus(values.status);
1371
+ const motionAttributes = compactObject({
1372
+ status: normalizedStatus,
1373
+ play_tags: values.tags !== undefined ? tagList(values.tags) : undefined
1374
+ });
1375
+
1376
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1377
+ const motion = await client.updateMotion(accountId, positionals[0], { motion: motionAttributes });
1378
+ if (values.json) return writeJson(context.stdout, motion);
1379
+
1380
+ writeLine(context.stdout, `Updated motion ${display(motion?.name)} (${display(motion?.prefix_id)}).`);
1381
+ renderMotion(motion, context);
939
1382
  }
940
1383
 
941
1384
  async function motionsStatusShortcut(action, args, context, { accountOverride } = {}) {
@@ -954,10 +1397,7 @@ async function motionsStatusShortcut(action, args, context, { accountOverride }
954
1397
  }
955
1398
 
956
1399
  async function updateMotionStatus(motionId, status, context, { accountOverride, json } = {}) {
957
- const normalizedStatus = String(status || "").trim().toLowerCase();
958
- if (!MOTION_STATUS_VALUES.has(normalizedStatus)) {
959
- throw new CommandError(MOTIONS_UPDATE_USAGE);
960
- }
1400
+ const normalizedStatus = normalizeMotionStatus(status);
961
1401
 
962
1402
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
963
1403
  const motion = await client.updateMotion(accountId, motionId, {
@@ -971,6 +1411,37 @@ async function updateMotionStatus(motionId, status, context, { accountOverride,
971
1411
  renderMotion(motion, context);
972
1412
  }
973
1413
 
1414
+ function normalizeMotionStatus(status) {
1415
+ if (status === undefined) return undefined;
1416
+
1417
+ const normalizedStatus = String(status || "").trim().toLowerCase();
1418
+ if (!MOTION_STATUS_VALUES.has(normalizedStatus)) {
1419
+ throw new CommandError(MOTIONS_UPDATE_USAGE);
1420
+ }
1421
+
1422
+ return normalizedStatus;
1423
+ }
1424
+
1425
+ async function motionsTagMutation(action, args, context, { accountOverride } = {}) {
1426
+ const usageText = action === "add" ? MOTIONS_ADD_TAG_USAGE : MOTIONS_REMOVE_TAG_USAGE;
1427
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1428
+ if (positionals.length !== 2) {
1429
+ throw new CommandError(usageText);
1430
+ }
1431
+
1432
+ const [motionId, tag] = positionals;
1433
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1434
+ const motion = action === "add" ?
1435
+ await client.addMotionTag(accountId, motionId, { tag }) :
1436
+ await client.removeMotionTag(accountId, motionId, { tag });
1437
+ if (values.json) return writeJson(context.stdout, motion);
1438
+
1439
+ const verb = action === "add" ? "Added" : "Removed";
1440
+ const preposition = action === "add" ? "to" : "from";
1441
+ writeLine(context.stdout, `${verb} tag ${display(tag)} ${preposition} motion ${display(motion?.name)} (${display(motion?.prefix_id)}).`);
1442
+ if (Array.isArray(motion?.play_tags)) writeLine(context.stdout, `Tags: ${display(motion.play_tags.join(", "), "-")}`);
1443
+ }
1444
+
974
1445
  async function motionsClone(args, context, { accountOverride } = {}) {
975
1446
  const { values, positionals } = parseCommandArgs(args, {
976
1447
  ...jsonOptions(),
@@ -1018,33 +1489,75 @@ async function motionsMoveProspects(args, context, { accountOverride } = {}) {
1018
1489
  }
1019
1490
 
1020
1491
  async function prospectsList(args, context, { accountOverride } = {}) {
1021
- const { values, positionals } = parseCommandArgs(args, {
1022
- ...jsonOptions(),
1023
- all: { type: "boolean" },
1024
- csv: { type: "boolean" },
1025
- query: { type: "string" },
1026
- company: { type: "string" },
1027
- "company-profile": { type: "string" },
1028
- motion: { type: "string" },
1029
- play: { type: "string" },
1030
- list: { type: "string" },
1031
- stage: { type: "string" },
1032
- "assigned-user": { type: "string" },
1033
- limit: { type: "string" },
1034
- offset: { type: "string" },
1492
+ const { values, positionals } = parseCommandArgs(args, prospectFilterOptions());
1493
+ if (positionals.length > 0) throw new CommandError("Usage: audienti prospects list [--json] [filters] [--account <acct_id>]");
1494
+ if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
1495
+ validateProspectFilterValues(values);
1496
+
1497
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
1498
+ const query = prospectQueryFromValues(values, config, { accountOverride });
1499
+ const payload = values.all ?
1500
+ await fetchAllProspects(client, accountId, query, { totalLimit: parseProspectTotalLimit(values.limit) }) :
1501
+ await client.prospects(accountId, query);
1502
+ if (values.json) return writeJson(context.stdout, payload);
1503
+ if (values.csv) return writeLine(context.stdout, prospectsToCsv(payload?.prospects || []));
1504
+
1505
+ renderProspects(payload, context, { wide: values.wide || values.all, profiles: values.profiles });
1506
+ }
1507
+
1508
+ async function prospectsCheck(args, context, { accountOverride } = {}) {
1509
+ const { values, positionals } = parseCommandArgs(args, prospectFilterOptions());
1510
+ if (positionals.length > 0) throw new CommandError(PROSPECTS_CHECK_USAGE);
1511
+ if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
1512
+ if (values.company || values["company-profile"]) throw new CommandError("Company filters are not supported for `prospects check`; it already finds prospects missing a certified company.");
1513
+ validateProspectFilterValues(values);
1514
+
1515
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
1516
+ const query = {
1517
+ ...prospectQueryFromValues(values, config, { accountOverride }),
1518
+ data_quality: "missing_certified_company"
1519
+ };
1520
+ const rawPayload = values.all ?
1521
+ await fetchAllProspects(client, accountId, query, { totalLimit: parseProspectTotalLimit(values.limit) }) :
1522
+ await client.prospects(accountId, query);
1523
+ const payload = withProspectAppUrls(rawPayload, client.host);
1524
+
1525
+ if (values.json) return writeJson(context.stdout, payload);
1526
+ if (values.csv) return writeLine(context.stdout, prospectCheckToCsv(payload?.prospects || []));
1527
+
1528
+ renderProspectCheck(payload, context);
1529
+ }
1530
+
1531
+ function prospectFilterOptions() {
1532
+ return {
1533
+ ...jsonOptions(),
1534
+ all: { type: "boolean" },
1535
+ csv: { type: "boolean" },
1536
+ query: { type: "string" },
1537
+ company: { type: "string" },
1538
+ "company-profile": { type: "string" },
1539
+ motion: { type: "string" },
1540
+ play: { type: "string" },
1541
+ list: { type: "string" },
1542
+ stage: { type: "string" },
1543
+ "assigned-user": { type: "string" },
1544
+ limit: { type: "string" },
1545
+ offset: { type: "string" },
1035
1546
  page: { type: "string" },
1036
1547
  profiles: { type: "boolean" },
1037
1548
  wide: { type: "boolean" }
1038
- });
1039
- if (positionals.length > 0) throw new CommandError("Usage: audienti prospects list [--json] [filters] [--account <acct_id>]");
1040
- if (values.csv && values.json) throw new CommandError("Choose one output format: use either --csv or --json.");
1549
+ };
1550
+ }
1551
+
1552
+ function validateProspectFilterValues(values) {
1041
1553
  if (values.page && values.offset) throw new CommandError("Choose one pagination mode: use either --page or --offset.");
1042
1554
  if (values.all && (values.page || values.offset)) throw new CommandError("--all cannot be combined with --page or --offset.");
1043
1555
  if (values.motion && values.play) throw new CommandError("Choose one motion filter: use either --motion or --play.");
1044
1556
  if (values.company && values["company-profile"]) throw new CommandError("Choose one company filter: use either --company or --company-profile.");
1557
+ }
1045
1558
 
1046
- const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
1047
- const query = compactObject({
1559
+ function prospectQueryFromValues(values, config, { accountOverride } = {}) {
1560
+ return compactObject({
1048
1561
  query: values.query,
1049
1562
  company: values.company,
1050
1563
  company_profile_id: values["company-profile"],
@@ -1058,13 +1571,6 @@ async function prospectsList(args, context, { accountOverride } = {}) {
1058
1571
  page: values.page,
1059
1572
  include_profiles: values.profiles
1060
1573
  });
1061
- const payload = values.all ?
1062
- await fetchAllProspects(client, accountId, query, { totalLimit: parseProspectTotalLimit(values.limit) }) :
1063
- await client.prospects(accountId, query);
1064
- if (values.json) return writeJson(context.stdout, payload);
1065
- if (values.csv) return writeLine(context.stdout, prospectsToCsv(payload?.prospects || []));
1066
-
1067
- renderProspects(payload, context, { wide: values.wide || values.all, profiles: values.profiles });
1068
1574
  }
1069
1575
 
1070
1576
  async function prospectsAssign(args, context, { accountOverride } = {}) {
@@ -1109,6 +1615,85 @@ async function prospectsShow(args, context, { accountOverride } = {}) {
1109
1615
  renderProspect(prospect, context);
1110
1616
  }
1111
1617
 
1618
+ async function prospectsDisposition(action, args, context, { accountOverride } = {}) {
1619
+ const usageText = {
1620
+ reject: PROSPECTS_REJECT_USAGE,
1621
+ nurture: PROSPECTS_NURTURE_USAGE,
1622
+ restore: PROSPECTS_RESTORE_USAGE
1623
+ }[action];
1624
+ const { values, positionals } = parseCommandArgs(args, {
1625
+ ...jsonOptions(),
1626
+ reason: { type: "string" }
1627
+ });
1628
+ if (positionals.length !== 1 || (action !== "nurture" && values.reason)) {
1629
+ throw new CommandError(usageText);
1630
+ }
1631
+
1632
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1633
+ const prospectId = positionals[0];
1634
+ const payload = action === "reject" ?
1635
+ await client.rejectProspect(accountId, prospectId) :
1636
+ action === "restore" ?
1637
+ await client.restoreProspect(accountId, prospectId) :
1638
+ await client.nurtureProspect(accountId, prospectId, compactObject({ inactive_reason: values.reason }));
1639
+ if (values.json) return writeJson(context.stdout, payload);
1640
+
1641
+ renderProspectDisposition(payload, context, { action });
1642
+ }
1643
+
1644
+ async function prospectsSetStatus(args, context, { accountOverride } = {}) {
1645
+ const { values, positionals } = parseCommandArgs(args, {
1646
+ ...jsonOptions(),
1647
+ status: { type: "string" }
1648
+ });
1649
+ const status = String(values.status || "").trim();
1650
+ if (positionals.length !== 1 || !PROSPECT_STATUS_VALUES.has(status)) {
1651
+ throw new CommandError(PROSPECTS_SET_STATUS_USAGE);
1652
+ }
1653
+
1654
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1655
+ const payload = await mutateProspectStatus(client, accountId, positionals[0], status);
1656
+ if (values.json) return writeJson(context.stdout, payload);
1657
+
1658
+ renderProspectDisposition(payload, context, { action: "set-status" });
1659
+ }
1660
+
1661
+ async function prospectsLock(args, context, { accountOverride } = {}) {
1662
+ const { values, positionals } = parseCommandArgs(args, {
1663
+ ...jsonOptions(),
1664
+ kind: { type: "string" },
1665
+ note: { type: "string" }
1666
+ });
1667
+ if (positionals.length !== 1) throw new CommandError(PROSPECTS_LOCK_USAGE);
1668
+
1669
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1670
+ const payload = await client.lockProspect(accountId, positionals[0], compactObject({
1671
+ lock_kind: values.kind,
1672
+ lock_note: values.note
1673
+ }));
1674
+ if (values.json) return writeJson(context.stdout, payload);
1675
+
1676
+ renderProspectDisposition(payload, context, { action: "lock" });
1677
+ }
1678
+
1679
+ async function prospectsUnlock(args, context, { accountOverride } = {}) {
1680
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1681
+ if (positionals.length !== 1) throw new CommandError(PROSPECTS_UNLOCK_USAGE);
1682
+
1683
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1684
+ const payload = await client.unlockProspect(accountId, positionals[0]);
1685
+ if (values.json) return writeJson(context.stdout, payload);
1686
+
1687
+ renderProspectDisposition(payload, context, { action: "unlock" });
1688
+ }
1689
+
1690
+ async function mutateProspectStatus(client, accountId, prospectId, status) {
1691
+ if (status === "active") return client.restoreProspect(accountId, prospectId);
1692
+ if (status === "rejected") return client.rejectProspect(accountId, prospectId);
1693
+
1694
+ return client.nurtureProspect(accountId, prospectId, { inactive_reason: status });
1695
+ }
1696
+
1112
1697
  async function prospectsTimeline(args, context, { accountOverride } = {}) {
1113
1698
  const { values, positionals } = parseCommandArgs(args, {
1114
1699
  ...jsonOptions(),
@@ -1784,6 +2369,18 @@ async function analyticsContent(args, context, { accountOverride } = {}) {
1784
2369
  renderAnalyticsContent(payload, context);
1785
2370
  }
1786
2371
 
2372
+ async function analyticsDashboard(args, context, { accountOverride } = {}) {
2373
+ const { values, positionals } = parseCommandArgs(args, analyticsDashboardOptions());
2374
+ if (positionals.length > 0) throw new CommandError(ANALYTICS_DASHBOARD_USAGE);
2375
+ validateDatePair(values["cohort-start"], values["cohort-end"], "--cohort-start", "--cohort-end");
2376
+
2377
+ const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
2378
+ const payload = await client.analyticsDashboard(accountId, analyticsDashboardQuery(values, config, { accountOverride }));
2379
+ if (values.json) return writeJson(context.stdout, payload);
2380
+
2381
+ renderAnalyticsDashboard(payload, context);
2382
+ }
2383
+
1787
2384
  function parseCommandArgs(args, options) {
1788
2385
  try {
1789
2386
  return parseArgs({
@@ -1955,6 +2552,20 @@ function analyticsUsersOptions() {
1955
2552
  };
1956
2553
  }
1957
2554
 
2555
+ function analyticsDashboardOptions() {
2556
+ return {
2557
+ ...jsonOptions(),
2558
+ "cohort-start": { type: "string" },
2559
+ "cohort-end": { type: "string" },
2560
+ "play-tag": { type: "string" },
2561
+ tag: { type: "string" },
2562
+ motion: { type: "string" },
2563
+ offer: { type: "string" },
2564
+ icp: { type: "string" },
2565
+ user: { type: "string" }
2566
+ };
2567
+ }
2568
+
1958
2569
  function analyticsQuery(values, config = {}, { accountOverride } = {}) {
1959
2570
  return compactObject({
1960
2571
  window: values.window,
@@ -1981,6 +2592,18 @@ function analyticsUsersQuery(values, config = {}, { accountOverride } = {}) {
1981
2592
  });
1982
2593
  }
1983
2594
 
2595
+ function analyticsDashboardQuery(values, config = {}, { accountOverride } = {}) {
2596
+ return compactObject({
2597
+ cohort_start_date: values["cohort-start"],
2598
+ cohort_end_date: values["cohort-end"],
2599
+ play_tag: values["play-tag"] || values.tag,
2600
+ motion_id: values.motion,
2601
+ offer_id: values.offer,
2602
+ icp_id: values.icp,
2603
+ account_user_id: resolveAccountUserId(values.user, config, { accountOverride })
2604
+ });
2605
+ }
2606
+
1984
2607
  function validateDatePair(start, end, startFlag, endFlag) {
1985
2608
  if ((start && !end) || (!start && end)) {
1986
2609
  throw new CommandError(`${startFlag} and ${endFlag} must be provided together.`);
@@ -2086,26 +2709,162 @@ function provenancePayload(provenance) {
2086
2709
 
2087
2710
  function compactObject(object) {
2088
2711
  return Object.fromEntries(
2089
- Object.entries(object).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== "")
2712
+ Object.entries(object).filter(([, value]) => {
2713
+ if (value === undefined || value === null) return false;
2714
+ if (Array.isArray(value)) return true;
2715
+
2716
+ return String(value).trim() !== "";
2717
+ })
2090
2718
  );
2091
2719
  }
2092
2720
 
2721
+ async function readLocalPackageMetadata() {
2722
+ const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
2723
+ return {
2724
+ name: packageJson.name || PACKAGE_NAME,
2725
+ version: packageJson.version
2726
+ };
2727
+ }
2728
+
2729
+ async function fetchLatestPackageVersion({ fetchImpl, registry }) {
2730
+ if (!fetchImpl) throw new Error("This Node runtime does not provide fetch.");
2731
+
2732
+ const response = await fetchImpl(registryLatestPackageUrl(registry), {
2733
+ headers: {
2734
+ accept: "application/vnd.npm.install-v1+json, application/json"
2735
+ }
2736
+ });
2737
+ const bodyText = await response.text();
2738
+ const body = bodyText ? JSON.parse(bodyText) : {};
2739
+
2740
+ if (!response.ok) {
2741
+ throw new Error(body?.error || body?.message || `Registry returned HTTP ${response.status}.`);
2742
+ }
2743
+
2744
+ const version = body?.version?.toString().trim();
2745
+ if (!version) throw new Error("Registry response did not include a version.");
2746
+
2747
+ return version;
2748
+ }
2749
+
2750
+ function registryLatestPackageUrl(registry) {
2751
+ const base = new URL(registry || DEFAULT_NPM_REGISTRY);
2752
+ if (!base.pathname.endsWith("/")) base.pathname = `${base.pathname}/`;
2753
+
2754
+ return new URL(`${encodeURIComponent(PACKAGE_NAME)}/latest`, base).toString();
2755
+ }
2756
+
2757
+ function updateCheckPayload({ currentVersion, latestVersion, status, updateAvailable, registry, error, now }) {
2758
+ return {
2759
+ kind: "update_check",
2760
+ package_name: PACKAGE_NAME,
2761
+ current_version: currentVersion,
2762
+ latest_version: latestVersion,
2763
+ update_available: updateAvailable,
2764
+ status,
2765
+ install_command: `npm install --global ${PACKAGE_NAME}`,
2766
+ registry,
2767
+ checked_at: now.toISOString(),
2768
+ error: error || null
2769
+ };
2770
+ }
2771
+
2772
+ function compareVersions(currentVersion, latestVersion) {
2773
+ const current = parseVersion(currentVersion);
2774
+ const latest = parseVersion(latestVersion);
2775
+
2776
+ for (let index = 0; index < 3; index += 1) {
2777
+ if (current.parts[index] !== latest.parts[index]) return current.parts[index] - latest.parts[index];
2778
+ }
2779
+
2780
+ if (current.prerelease === latest.prerelease) return 0;
2781
+ if (!current.prerelease) return 1;
2782
+ if (!latest.prerelease) return -1;
2783
+
2784
+ return current.prerelease.localeCompare(latest.prerelease);
2785
+ }
2786
+
2787
+ function parseVersion(version) {
2788
+ const [core, prerelease = ""] = String(version || "").split("-", 2);
2789
+ const parts = core.split(".").map((part) => Number.parseInt(part, 10));
2790
+
2791
+ return {
2792
+ parts: [parts[0] || 0, parts[1] || 0, parts[2] || 0],
2793
+ prerelease
2794
+ };
2795
+ }
2796
+
2797
+ function tagList(value) {
2798
+ if (value === undefined) return undefined;
2799
+
2800
+ return String(value)
2801
+ .split(/[\r\n,;]+/)
2802
+ .map((tag) => tag.trim().toLowerCase())
2803
+ .filter(Boolean)
2804
+ .filter((tag, index, tags) => tags.indexOf(tag) === index);
2805
+ }
2806
+
2807
+ function companyRuleOptions() {
2808
+ return {
2809
+ ...jsonOptions(),
2810
+ name: { type: "string" },
2811
+ "linkedin-url": { type: "string" },
2812
+ domain: { type: "string" },
2813
+ disposition: { type: "string" },
2814
+ user: { type: "string" },
2815
+ note: { type: "string" }
2816
+ };
2817
+ }
2818
+
2819
+ function companyRulePayload(values) {
2820
+ const userValue = values.user === undefined ? undefined : String(values.user || "").trim();
2821
+ const userCleared = userValue && ["none", "account", "all"].includes(userValue.toLowerCase());
2822
+
2823
+ return compactObject({
2824
+ name: values.name,
2825
+ linkedin_company_url: values["linkedin-url"],
2826
+ domain: values.domain,
2827
+ disposition: values.disposition,
2828
+ note: values.note,
2829
+ scope_kind: userValue === undefined ? undefined : (userCleared ? "account" : "account_user"),
2830
+ account_user_id: userValue === undefined ? undefined : (userCleared ? "" : userValue)
2831
+ });
2832
+ }
2833
+
2834
+ function parseDncImportValues(body) {
2835
+ return String(body || "")
2836
+ .split(/\r?\n/)
2837
+ .map((line) => line.trim())
2838
+ .filter(Boolean)
2839
+ .map((line) => line.split(",")[0])
2840
+ .map((value) => value.trim().replace(/^"|"$/g, ""))
2841
+ .filter(Boolean);
2842
+ }
2843
+
2844
+ function filterRecordsByTag(records, value, field) {
2845
+ const tag = tagList(value)?.[0];
2846
+ if (!tag) return records;
2847
+
2848
+ return Array.isArray(records) ? records.filter((record) => (tagList(record?.[field]) || []).includes(tag)) : [];
2849
+ }
2850
+
2093
2851
  async function icpCreatePayload(values) {
2094
2852
  if (values.payload) {
2095
- if (values.name || values.notes || values["discovery-keyword"]) {
2096
- throw new CommandError("Choose one ICP input mode: either --payload <file.json> or the simple --name/--notes/--discovery-keyword flags.");
2853
+ if (values.name || values.notes || values.tags !== undefined || values["discovery-keyword"]) {
2854
+ throw new CommandError("Choose one ICP input mode: either --payload <file.json> or the simple --name/--notes/--discovery-keyword/tags flags.");
2097
2855
  }
2098
2856
 
2099
2857
  return readJsonPayload(values.payload);
2100
2858
  }
2101
2859
 
2102
2860
  if (!values.name) {
2103
- throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] | --payload <file.json>) [--json] [--account <acct_id>]");
2861
+ throw new CommandError("Usage: audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] | --payload <file.json>) [--json] [--account <acct_id>]");
2104
2862
  }
2105
2863
 
2106
2864
  return compactObject({
2107
2865
  name: values.name,
2108
2866
  notes: values.notes,
2867
+ tags: values.tags !== undefined ? tagList(values.tags) : undefined,
2109
2868
  discovery_keyword: values["discovery-keyword"]
2110
2869
  });
2111
2870
  }
@@ -2246,6 +3005,17 @@ function normalizePositiveInteger(value) {
2246
3005
  return parsed;
2247
3006
  }
2248
3007
 
3008
+ function normalizeOptionalPositiveInteger(value, flagName) {
3009
+ if (value === undefined || value === null || value === "") return undefined;
3010
+
3011
+ const parsed = normalizePositiveInteger(value);
3012
+ if (parsed === null || String(parsed) !== String(value).trim()) {
3013
+ throw new CommandError(`${flagName} must be a positive integer.`);
3014
+ }
3015
+
3016
+ return parsed;
3017
+ }
3018
+
2249
3019
  function normalizeLookupType(value) {
2250
3020
  const normalized = String(value || "").trim().toLowerCase();
2251
3021
  if (normalized === "email" || normalized === "phone") return normalized;
@@ -2394,6 +3164,22 @@ function writeJson(stream, value) {
2394
3164
  writeLine(stream, JSON.stringify(value, null, 2));
2395
3165
  }
2396
3166
 
3167
+ function renderUpdateCheck(payload, context) {
3168
+ writeLine(context.stdout, `Package: ${payload.package_name}`);
3169
+ writeLine(context.stdout, `Current version: ${display(payload.current_version, "-")}`);
3170
+ writeLine(context.stdout, `Latest version: ${display(payload.latest_version, "unknown")}`);
3171
+
3172
+ if (payload.status === "update_available") {
3173
+ writeLine(context.stdout, "Status: update available");
3174
+ writeLine(context.stdout, `Update: ${payload.install_command}`);
3175
+ } else if (payload.status === "current") {
3176
+ writeLine(context.stdout, "Status: current");
3177
+ } else {
3178
+ writeLine(context.stdout, "Status: unknown");
3179
+ if (payload.error) writeLine(context.stdout, `Error: ${payload.error}`);
3180
+ }
3181
+ }
3182
+
2397
3183
  async function sleep(milliseconds) {
2398
3184
  await new Promise((resolve) => setTimeout(resolve, milliseconds));
2399
3185
  }
@@ -2410,9 +3196,48 @@ function renderLists(lists, context) {
2410
3196
  function renderList(list, context) {
2411
3197
  writeLine(context.stdout, `List: ${display(list?.name)} (${display(list?.prefix_id)})`);
2412
3198
  writeLine(context.stdout, `Prospects: ${display(list?.prospect_count, 0)}`);
3199
+ if (Array.isArray(list?.tags)) writeLine(context.stdout, `Tags: ${display(list.tags.join(", "), "-")}`);
2413
3200
  if (list?.description) writeLine(context.stdout, `Description: ${list.description}`);
2414
3201
  }
2415
3202
 
3203
+ function renderTags(tags, context) {
3204
+ if (!Array.isArray(tags) || tags.length === 0) return writeLine(context.stdout, "No tags found.");
3205
+
3206
+ writeLine(context.stdout, "TAG\tICPS\tLISTS\tMOTIONS\tTOTAL");
3207
+ for (const tag of tags) {
3208
+ writeLine(
3209
+ context.stdout,
3210
+ [
3211
+ display(tag.name),
3212
+ display(tag.icp_count, 0),
3213
+ display(tag.list_count, 0),
3214
+ display(tag.motion_count, 0),
3215
+ display(tag.total_count, 0)
3216
+ ].join("\t")
3217
+ );
3218
+ }
3219
+ }
3220
+
3221
+ function renderTagDetails(payload, context) {
3222
+ const icps = Array.isArray(payload?.icps) ? payload.icps : [];
3223
+ const lists = Array.isArray(payload?.lists) ? payload.lists : [];
3224
+ const motions = Array.isArray(payload?.motions) ? payload.motions : [];
3225
+
3226
+ writeLine(context.stdout, `Tag: ${display(payload?.tag)}`);
3227
+ writeLine(context.stdout, `ICPs: ${icps.length}`);
3228
+ writeLine(context.stdout, `Lists: ${lists.length}`);
3229
+ writeLine(context.stdout, `Motions: ${motions.length}`);
3230
+ writeLine(context.stdout);
3231
+ writeLine(context.stdout, "ICPs");
3232
+ renderIcps(icps, context);
3233
+ writeLine(context.stdout);
3234
+ writeLine(context.stdout, "Lists");
3235
+ renderLists(lists, context);
3236
+ writeLine(context.stdout);
3237
+ writeLine(context.stdout, "Motions");
3238
+ renderMotions(motions, context);
3239
+ }
3240
+
2416
3241
  function renderUsers(users, context) {
2417
3242
  if (!Array.isArray(users) || users.length === 0) return writeLine(context.stdout, "No account users found.");
2418
3243
 
@@ -2482,16 +3307,23 @@ function renderOffers(offers, context) {
2482
3307
  }
2483
3308
  }
2484
3309
 
3310
+ function renderOffer(offer, context) {
3311
+ writeLine(context.stdout, `Offer: ${display(offer?.name)} (${display(offer?.prefix_id)})`);
3312
+ if (offer?.description) writeLine(context.stdout, `Description: ${offer.description}`);
3313
+ if (offer?.url) writeLine(context.stdout, `URL: ${offer.url}`);
3314
+ }
3315
+
2485
3316
  function renderIcps(icps, context) {
2486
3317
  if (!Array.isArray(icps) || icps.length === 0) return writeLine(context.stdout, "No ICPs found.");
2487
3318
 
2488
- writeLine(context.stdout, "ICP ID\tNAME\tDISCOVERY KEYWORD\tAGENT");
3319
+ writeLine(context.stdout, "ICP ID\tNAME\tTAGS\tDISCOVERY KEYWORD\tAGENT");
2489
3320
  for (const icp of icps) {
2490
3321
  writeLine(
2491
3322
  context.stdout,
2492
3323
  [
2493
3324
  display(icp.prefix_id),
2494
3325
  display(icp.name),
3326
+ display(Array.isArray(icp.tags) && icp.tags.length > 0 ? icp.tags.join(",") : "-"),
2495
3327
  display(icp.discovery_keyword),
2496
3328
  display(icp.agent?.name)
2497
3329
  ].join("\t")
@@ -2499,6 +3331,14 @@ function renderIcps(icps, context) {
2499
3331
  }
2500
3332
  }
2501
3333
 
3334
+ function renderIcp(icp, context) {
3335
+ writeLine(context.stdout, `ICP: ${display(icp?.name)} (${display(icp?.prefix_id)})`);
3336
+ if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
3337
+ if (icp?.notes) writeLine(context.stdout, `Notes: ${icp.notes}`);
3338
+ if (icp?.discovery_keyword) writeLine(context.stdout, `Discovery keyword: ${icp.discovery_keyword}`);
3339
+ if (icp?.agent?.name) writeLine(context.stdout, `Agent: ${icp.agent.name}`);
3340
+ }
3341
+
2502
3342
  function renderCompanies(payload, context) {
2503
3343
  const companies = Array.isArray(payload?.companies) ? payload.companies : [];
2504
3344
  if (companies.length === 0) return writeLine(context.stdout, "No companies found.");
@@ -2519,6 +3359,54 @@ function renderCompanies(payload, context) {
2519
3359
  }
2520
3360
  }
2521
3361
 
3362
+ function renderDncEntries(payload, context) {
3363
+ const entries = Array.isArray(payload?.dnc_entries) ? payload.dnc_entries : [];
3364
+ if (entries.length === 0) return writeLine(context.stdout, "No DNC entries found.");
3365
+
3366
+ writeLine(context.stdout, "DNC ID\tKIND\tVALUE\tIDENTIFIER\tSOURCE\tPROSPECT");
3367
+ for (const entry of entries) {
3368
+ writeLine(
3369
+ context.stdout,
3370
+ [
3371
+ display(entry.prefix_id || entry.id),
3372
+ display(entry.key_kind),
3373
+ display(entry.canonical_value || entry.citation_id),
3374
+ display(entry.identifier),
3375
+ display(entry.source_kind),
3376
+ display(entry.prospect_id)
3377
+ ].join("\t")
3378
+ );
3379
+ }
3380
+ }
3381
+
3382
+ function renderCompanyRules(payload, context) {
3383
+ const rules = Array.isArray(payload?.company_rules) ? payload.company_rules : [];
3384
+ if (rules.length === 0) return writeLine(context.stdout, "No company rules found.");
3385
+
3386
+ writeLine(context.stdout, "RULE ID\tCOMPANY\tDOMAIN\tDISPOSITION\tSCOPE\tACTIVE");
3387
+ for (const rule of rules) {
3388
+ writeLine(
3389
+ context.stdout,
3390
+ [
3391
+ display(rule.prefix_id || rule.id),
3392
+ display(rule.name || rule.linkedin_company_identifier || rule.linkedin_company_url),
3393
+ display(rule.domain),
3394
+ display(rule.disposition),
3395
+ companyRuleScopeLabel(rule),
3396
+ rule.active ? "yes" : "no"
3397
+ ].join("\t")
3398
+ );
3399
+ }
3400
+ }
3401
+
3402
+ function companyRuleScopeLabel(rule) {
3403
+ if (rule?.scope_kind === "account_user") {
3404
+ return `user:${display(rule.account_user_email || rule.account_user_id)}`;
3405
+ }
3406
+
3407
+ return "account";
3408
+ }
3409
+
2522
3410
  async function performBulkMutation(perform) {
2523
3411
  try {
2524
3412
  return { payload: await perform(), rejected: false };
@@ -2563,6 +3451,7 @@ function renderMotion(motion, context) {
2563
3451
  if (motion?.offer?.name) writeLine(context.stdout, `Offer: ${motion.offer.name} (${display(motion.offer.prefix_id)})`);
2564
3452
  if (motion?.icp?.name) writeLine(context.stdout, `ICP: ${motion.icp.name} (${display(motion.icp.prefix_id)})`);
2565
3453
  if (motion?.list?.name) writeLine(context.stdout, `List: ${motion.list.name} (${display(motion.list.prefix_id)})`);
3454
+ if (Array.isArray(motion?.play_tags)) writeLine(context.stdout, `Tags: ${display(motion.play_tags.join(", "), "-")}`);
2566
3455
  if (motion?.principal_account_user?.id) {
2567
3456
  writeLine(
2568
3457
  context.stdout,
@@ -2630,6 +3519,27 @@ function renderProspects(payload, context, { wide = false, profiles = false } =
2630
3519
  }
2631
3520
  }
2632
3521
 
3522
+ function renderProspectCheck(payload, context) {
3523
+ const prospects = Array.isArray(payload?.prospects) ? payload.prospects : [];
3524
+ const totalCount = display(payload?.meta?.total_count, prospects.length);
3525
+ if (prospects.length === 0) return writeLine(context.stdout, "No suspect prospects found.");
3526
+
3527
+ writeLine(context.stdout, `Suspect prospects: ${totalCount}`);
3528
+ writeLine(context.stdout, "PROSPECT ID\tSTAGE\tNAME\tREPORTED COMPANY\tCERTIFIED\tREASON\tURL");
3529
+ for (const prospect of prospects) {
3530
+ const certification = prospect.company_certification || {};
3531
+ writeLine(context.stdout, [
3532
+ display(prospect.prefix_id),
3533
+ display(prospect.account_prospect?.pipeline_stage),
3534
+ display(prospect.display_name || prospect.name),
3535
+ display(certification.reported_company || prospect.company),
3536
+ certification.status === "certified" ? "yes" : "no",
3537
+ display(certification.reason, "missing_employment_citation"),
3538
+ display(prospect.app_url)
3539
+ ].join("\t"));
3540
+ }
3541
+ }
3542
+
2633
3543
  function renderProspect(prospect, context) {
2634
3544
  writeLine(context.stdout, `Prospect: ${display(prospect?.display_name || prospect?.name)} (${display(prospect?.prefix_id)})`);
2635
3545
  if (prospect?.company) writeLine(context.stdout, `Company: ${prospect.company}`);
@@ -2639,6 +3549,27 @@ function renderProspect(prospect, context) {
2639
3549
  if (nextActionLabel(prospect?.queue)) writeLine(context.stdout, `Next action: ${nextActionLabel(prospect.queue)}`);
2640
3550
  }
2641
3551
 
3552
+ function renderProspectDisposition(payload, context, { action }) {
3553
+ const prospect = payload?.prospect || {};
3554
+ const accountProspect = payload?.account_prospect || {};
3555
+ const actionLabel = {
3556
+ reject: "Rejected",
3557
+ nurture: "Moved to nurture",
3558
+ restore: "Restored",
3559
+ "set-status": "Set status for",
3560
+ lock: "Locked",
3561
+ unlock: "Unlocked"
3562
+ }[action] || display(payload?.status, "Updated");
3563
+
3564
+ writeLine(context.stdout, `${actionLabel} prospect ${display(prospect.display_name || prospect.name)} (${display(prospect.prefix_id)}).`);
3565
+ if (accountProspect.status) writeLine(context.stdout, `Status: ${accountProspect.status}`);
3566
+ if (accountProspect.inactive_reason) writeLine(context.stdout, `Inactive reason: ${accountProspect.inactive_reason}`);
3567
+ if (accountProspect.locked_at) writeLine(context.stdout, `Locked at: ${accountProspect.locked_at}`);
3568
+ if (accountProspect.lock_kind) writeLine(context.stdout, `Lock kind: ${accountProspect.lock_kind}`);
3569
+ if (accountProspect.lock_note) writeLine(context.stdout, `Lock note: ${accountProspect.lock_note}`);
3570
+ if (payload?.system_list?.name) writeLine(context.stdout, `List: ${payload.system_list.name} (${display(payload.system_list.prefix_id)})`);
3571
+ }
3572
+
2642
3573
  function renderProspectTimeline(payload, context) {
2643
3574
  const prospect = payload?.prospect || {};
2644
3575
  const timeline = Array.isArray(payload?.timeline) ? payload.timeline : [];
@@ -3161,6 +4092,32 @@ function renderMotionAnalytics(payload, context) {
3161
4092
  writeCountTable(context, "Prospect cohorts by produced day", payload?.prospects_by_day, ["DATE", "PRODUCED", "ACTIVE", "ACTIVE %", "INACTIVE", "STAGES"], dailyProspectRow);
3162
4093
  }
3163
4094
 
4095
+ function renderMotionDiscoveryRun(payload, context) {
4096
+ const motion = payload?.motion || {};
4097
+ const label = entityLabel(motion) || payload?.motion_id;
4098
+ const prefix = payload?.enqueued ? "Discovery queued" : "Discovery not queued";
4099
+ writeLine(context.stdout, `${prefix} for ${display(label)}.`);
4100
+ writeLine(context.stdout, `Reason: ${display(payload?.reason)}`);
4101
+ writeLine(context.stdout, `Target count: ${display(payload?.target_count)}`);
4102
+ }
4103
+
4104
+ function renderAnalyticsDashboard(payload, context) {
4105
+ writeLine(context.stdout, `Dashboard analytics (${display(payload?.cohort?.label, "selected cohort")})`);
4106
+ if (payload?.cohort) {
4107
+ writeLine(context.stdout, `Cohort: ${payload.cohort.start_date} to ${payload.cohort.end_date} (${display(payload.cohort.field, "account_prospects.created_at")})`);
4108
+ }
4109
+ if (payload?.activity) {
4110
+ writeLine(context.stdout, `Activity: ${payload.activity.start_date} to ${payload.activity.end_date} (${display(payload.activity.field, "events.created_at")})`);
4111
+ }
4112
+ writeDashboardFilters(payload, context);
4113
+ writeLine(context.stdout, `Prospects: ${display(payload?.cohort_size, 0)}`);
4114
+ writeLine(context.stdout, `Companies: ${display(payload?.cohort_company_target_count, 0)}`);
4115
+ writeLine(context.stdout, `People/company: ${display(payload?.cohort_people_per_company_average, "0.0")}`);
4116
+ writeLine(context.stdout, `Active: ${display(payload?.active_cohort_count, 0)} (${display(payload?.active_cohort_company_target_count, 0)} companies, ${percentageLabel(payload?.active_cohort_percentage)})`);
4117
+ writeLine(context.stdout, `Inactive: ${display(payload?.inactive_cohort_count, 0)}`);
4118
+ writeCountTable(context, "Current pipeline stages", payload?.pipeline_stage_counts, ["STAGE", "COUNT"], countRow);
4119
+ }
4120
+
3164
4121
  function renderAnalyticsProspectCohortAnalysis(payload, context) {
3165
4122
  const cohorts = Array.isArray(payload?.cohorts) ? payload.cohorts : [];
3166
4123
  writeLine(context.stdout, `Prospect cohort analysis (${display(payload?.weeks, cohorts.length)} weeks)`);
@@ -3235,7 +4192,20 @@ function writeAnalyticsScope(payload, context) {
3235
4192
  }
3236
4193
  }
3237
4194
 
3238
- function writeAnalyticsPlatform(payload, context) {
4195
+ function writeDashboardFilters(payload, context) {
4196
+ const filters = payload?.filters || {};
4197
+ if (filters.motion) writeLine(context.stdout, `Motion: ${entityLabel(filters.motion)}`);
4198
+ if (filters.play_tag) writeLine(context.stdout, `Tag: ${filters.play_tag}`);
4199
+ if (filters.offer) writeLine(context.stdout, `Offer: ${entityLabel(filters.offer)}`);
4200
+ if (filters.icp) writeLine(context.stdout, `ICP: ${entityLabel(filters.icp)}`);
4201
+ if (filters.account_user) {
4202
+ writeLine(context.stdout, `User: ${entityLabel(filters.account_user)}`);
4203
+ } else {
4204
+ writeLine(context.stdout, "User: all account users");
4205
+ }
4206
+ }
4207
+
4208
+ function writeAnalyticsPlatform(payload, context) {
3239
4209
  if (!payload?.platform) return;
3240
4210
 
3241
4211
  const label = display(payload.platform.label, payload.platform.key);
@@ -3674,6 +4644,77 @@ function prospectsToCsv(prospects) {
3674
4644
  ].join("\n");
3675
4645
  }
3676
4646
 
4647
+ function prospectCheckToCsv(prospects) {
4648
+ const headers = [
4649
+ "prefix_id",
4650
+ "display_name",
4651
+ "name",
4652
+ "title",
4653
+ "reported_company",
4654
+ "company_certification_status",
4655
+ "company_certification_reason",
4656
+ "email",
4657
+ "linkedin_url",
4658
+ "app_url",
4659
+ "account_prospect_status",
4660
+ "pipeline_stage",
4661
+ "assigned_to_account_user_id",
4662
+ "motion_prefix_id",
4663
+ "motion_name",
4664
+ "updated_at"
4665
+ ];
4666
+
4667
+ const rows = prospects.map((prospect) => {
4668
+ const certification = prospect.company_certification || {};
4669
+ return {
4670
+ prefix_id: prospect.prefix_id,
4671
+ display_name: prospect.display_name,
4672
+ name: prospect.name,
4673
+ title: prospect.title,
4674
+ reported_company: certification.reported_company || prospect.company,
4675
+ company_certification_status: certification.status,
4676
+ company_certification_reason: certification.reason,
4677
+ email: prospect.email,
4678
+ linkedin_url: prospect.linkedin_url,
4679
+ app_url: prospect.app_url,
4680
+ account_prospect_status: prospect.account_prospect?.status,
4681
+ pipeline_stage: prospect.account_prospect?.pipeline_stage,
4682
+ assigned_to_account_user_id: prospect.account_prospect?.assigned_to_account_user_id,
4683
+ motion_prefix_id: prospect.account_prospect?.motion?.prefix_id,
4684
+ motion_name: prospect.account_prospect?.motion?.name,
4685
+ updated_at: prospect.updated_at
4686
+ };
4687
+ });
4688
+
4689
+ return [
4690
+ headers.join(","),
4691
+ ...rows.map((row) => headers.map((header) => csvField(row[header])).join(","))
4692
+ ].join("\n");
4693
+ }
4694
+
4695
+ function withProspectAppUrls(payload, host) {
4696
+ if (!payload || !Array.isArray(payload.prospects)) return payload;
4697
+
4698
+ return {
4699
+ ...payload,
4700
+ prospects: payload.prospects.map((prospect) => ({
4701
+ ...prospect,
4702
+ app_url: prospectAppUrl(prospect, host)
4703
+ }))
4704
+ };
4705
+ }
4706
+
4707
+ function prospectAppUrl(prospect, host) {
4708
+ const prospectId = String(prospect?.prefix_id || "").trim();
4709
+ if (!prospectId) return undefined;
4710
+
4711
+ try {
4712
+ return new URL(`/prospects/${encodeURIComponent(prospectId)}`, normalizeHost(host)).toString();
4713
+ } catch {
4714
+ return undefined;
4715
+ }
4716
+ }
4717
+
3677
4718
  function sequenceExportRowsToCsv(rows) {
3678
4719
  return [
3679
4720
  SEQUENCE_EXPORT_CSV_COLUMNS.join(","),
@@ -3774,6 +4815,7 @@ const HELP_TOPICS = new Map([
3774
4815
  " Setup & identity",
3775
4816
  " audienti auth status",
3776
4817
  " audienti config list",
4818
+ " audienti update check",
3777
4819
  " audienti users list",
3778
4820
  " audienti users select <account_user_id|email|name|me>",
3779
4821
  " audienti users activity [account_user_id|me]",
@@ -3782,9 +4824,12 @@ const HELP_TOPICS = new Map([
3782
4824
  " audienti motions list",
3783
4825
  " audienti motions show <motn_id>",
3784
4826
  " audienti motions analytics <motn_id>",
4827
+ " audienti motions run-discovery <motn_id>",
3785
4828
  " audienti motions prospects <motn_id>",
3786
4829
  " audienti motions create --payload <file.json>",
3787
- " audienti motions update <motn_id> --status <state>",
4830
+ " audienti motions update <motn_id> [--status <state>] [--tags <tag[,tag...]>]",
4831
+ " audienti motions add-tag <motn_id> <tag>",
4832
+ " audienti motions remove-tag <motn_id> <tag>",
3788
4833
  " audienti motions activate <motn_id>",
3789
4834
  " audienti motions pause <motn_id>",
3790
4835
  " audienti motions delete <motn_id> --confirm <yes|true|Y|y>",
@@ -3794,8 +4839,15 @@ const HELP_TOPICS = new Map([
3794
4839
  "",
3795
4840
  " Prospects",
3796
4841
  " audienti prospects list [filters]",
4842
+ " audienti prospects check [filters]",
3797
4843
  " audienti prospects show <prsp_id>",
3798
4844
  " audienti prospects assign <prsp_id> --assigned-user <id|me|unassign>",
4845
+ " audienti prospects set-status <prsp_id> --status <active|nurture|non_responsive|not_fit|rejected>",
4846
+ " audienti prospects lock <prsp_id> [--note <text>]",
4847
+ " audienti prospects reject <prsp_id>",
4848
+ " audienti prospects nurture <prsp_id> [--reason <reason>]",
4849
+ " audienti prospects restore <prsp_id>",
4850
+ " audienti prospects unlock <prsp_id>",
3799
4851
  " audienti prospects timeline <prsp_id>",
3800
4852
  " audienti prospects import <linkedin_url> [--motion <motn_id>]",
3801
4853
  " audienti prospects import-batch --file <csv|jsonl|json>",
@@ -3803,11 +4855,26 @@ const HELP_TOPICS = new Map([
3803
4855
  " audienti prospects add-profile <prsp_id> --url <profile_url|email|phone>",
3804
4856
  "",
3805
4857
  " Lists & targeting inputs",
3806
- " audienti lists list",
4858
+ " audienti lists list [--tag <tag>]",
3807
4859
  " audienti lists prospects <list_id>",
4860
+ " audienti lists add-tag <list_id> <tag>",
4861
+ " audienti lists remove-tag <list_id> <tag>",
4862
+ " audienti tags list",
4863
+ " audienti tags show <tag>",
3808
4864
  " audienti offers list",
3809
- " audienti icps list",
4865
+ " audienti offers show <offr_id>",
4866
+ " audienti offers update <offr_id> [--name <text>]",
4867
+ " audienti offers delete <offr_id> --confirm <yes|true|Y|y>",
4868
+ " audienti icps list [--tag <tag>]",
4869
+ " audienti icps show <icp_id>",
4870
+ " audienti icps update <icp_id> [--tags <tag[,tag...]>]",
4871
+ " audienti icps add-tag <icp_id> <tag>",
4872
+ " audienti icps remove-tag <icp_id> <tag>",
3810
4873
  " audienti companies search --query <text>",
4874
+ " audienti dnc list",
4875
+ " audienti dnc add <email|citation_id|profile_url>",
4876
+ " audienti company-rules list",
4877
+ " audienti company-rules create (--linkedin-url <url> | --domain <domain>) --disposition <state>",
3811
4878
  "",
3812
4879
  " Writer",
3813
4880
  " audienti writer test-run <prsp_id>",
@@ -3821,6 +4888,7 @@ const HELP_TOPICS = new Map([
3821
4888
  "",
3822
4889
  " Analytics",
3823
4890
  " audienti analytics prospects --window 24h",
4891
+ " audienti analytics dashboard --play-tag <tag>",
3824
4892
  " audienti analytics prospects cohort-analysis --weeks 4 --motion <motn_id>",
3825
4893
  " audienti analytics users --user me --window 30d",
3826
4894
  " audienti analytics visibility --window 24h --user me",
@@ -3835,6 +4903,7 @@ const HELP_TOPICS = new Map([
3835
4903
  " Inspect a prospect: audienti prospects show <prsp_id> --json",
3836
4904
  " Preview a campaign: audienti writer test-run <prsp_id>",
3837
4905
  " Analyze one motion: audienti motions analytics <motn_id>",
4906
+ " Count one campaign: audienti analytics dashboard --play-tag <tag>",
3838
4907
  " Audit your work: audienti analytics users --user me --window 30d",
3839
4908
  "",
3840
4909
  "Global options:",
@@ -3935,6 +5004,46 @@ const HELP_TOPICS = new Map([
3935
5004
  " Default account user: account user name and id, or none selected"
3936
5005
  ].join("\n")],
3937
5006
 
5007
+ ["update", [
5008
+ "Usage:",
5009
+ " audienti update check [--json] [--registry <url>]",
5010
+ "",
5011
+ "Status: implemented",
5012
+ "",
5013
+ "Purpose:",
5014
+ " Check whether this local Audienti CLI install is behind the latest published package.",
5015
+ "",
5016
+ "Commands:",
5017
+ " audienti update check Compare the local package version to the npm registry"
5018
+ ].join("\n")],
5019
+
5020
+ ["update check", [
5021
+ "Usage:",
5022
+ " audienti update check [--json] [--registry <url>]",
5023
+ "",
5024
+ "Status: implemented",
5025
+ "",
5026
+ "Purpose:",
5027
+ " Report whether this local CLI should be updated.",
5028
+ "",
5029
+ "Output shape:",
5030
+ " package_name: @audienti/cli",
5031
+ " current_version: local package version",
5032
+ " latest_version: latest registry version, or null when unknown",
5033
+ " update_available: true | false | null",
5034
+ " status: current | update_available | unknown",
5035
+ " install_command: npm install --global @audienti/cli",
5036
+ " registry: registry URL used for the check",
5037
+ " checked_at: ISO timestamp",
5038
+ " error: string | null",
5039
+ "",
5040
+ "Options:",
5041
+ " --registry <url> Alternate npm-compatible registry. Default: https://registry.npmjs.org",
5042
+ "",
5043
+ "Example:",
5044
+ " audienti update check --json"
5045
+ ].join("\n")],
5046
+
3938
5047
  ["accounts", [
3939
5048
  "Usage:",
3940
5049
  " audienti accounts list [--json]",
@@ -4047,12 +5156,15 @@ const HELP_TOPICS = new Map([
4047
5156
  ["offers", [
4048
5157
  "Usage:",
4049
5158
  " audienti offers list [--json]",
5159
+ " audienti offers show <offr_id> [--json]",
4050
5160
  " audienti offers create --name <text> [--json]",
5161
+ " audienti offers update <offr_id> [--name <text>] [--json]",
5162
+ " audienti offers delete <offr_id> --confirm <yes|true|Y|y> [--json]",
4051
5163
  "",
4052
5164
  "Status: implemented",
4053
5165
  "",
4054
5166
  "Purpose:",
4055
- " List the offers available to the current account so an agent can choose offer_id for motion creation."
5167
+ " Manage the offers available to the current account so an agent can choose offer_id for motion creation."
4056
5168
  ].join("\n")],
4057
5169
 
4058
5170
  ["offers list", [
@@ -4102,10 +5214,60 @@ const HELP_TOPICS = new Map([
4102
5214
  " }"
4103
5215
  ].join("\n")],
4104
5216
 
5217
+ ["offers show", [
5218
+ "Usage:",
5219
+ ` ${OFFERS_SHOW_USAGE.slice("Usage: ".length)}`,
5220
+ "",
5221
+ "Status: implemented",
5222
+ "",
5223
+ "Input shape:",
5224
+ " offr_id: offr_ prefixed id or integer id",
5225
+ "",
5226
+ "API:",
5227
+ " GET /api/v1/accounts/:account_id/offers/:id.json"
5228
+ ].join("\n")],
5229
+
5230
+ ["offers update", [
5231
+ "Usage:",
5232
+ ` ${OFFERS_UPDATE_USAGE.slice("Usage: ".length)}`,
5233
+ "",
5234
+ "Status: implemented",
5235
+ "",
5236
+ "Purpose:",
5237
+ " Update simple offer fields without changing linked motions.",
5238
+ "",
5239
+ "Input shape:",
5240
+ " offr_id: offr_ prefixed id or integer id",
5241
+ " name: string | optional",
5242
+ " description: string | optional",
5243
+ " url: string | optional",
5244
+ "",
5245
+ "API:",
5246
+ " PATCH /api/v1/accounts/:account_id/offers/:id.json"
5247
+ ].join("\n")],
5248
+
5249
+ ["offers delete", [
5250
+ "Usage:",
5251
+ ` ${OFFERS_DELETE_USAGE.slice("Usage: ".length)}`,
5252
+ "",
5253
+ "Status: implemented",
5254
+ "",
5255
+ "Input shape:",
5256
+ " offr_id: offr_ prefixed id or integer id",
5257
+ " confirm: one of yes, true, Y, y",
5258
+ "",
5259
+ "API:",
5260
+ " DELETE /api/v1/accounts/:account_id/offers/:id.json"
5261
+ ].join("\n")],
5262
+
4105
5263
  ["icps", [
4106
5264
  "Usage:",
4107
- " audienti icps list [--json]",
5265
+ " audienti icps list [--tag <tag>] [--json]",
5266
+ " audienti icps show <icp_id> [--json]",
4108
5267
  " audienti icps create (--name <text> | --payload <file.json>) [--json]",
5268
+ " audienti icps update <icp_id> [--tags <tag[,tag...]>] [--json]",
5269
+ " audienti icps add-tag <icp_id> <tag> [--json]",
5270
+ " audienti icps remove-tag <icp_id> <tag> [--json]",
4109
5271
  "",
4110
5272
  "Status: implemented",
4111
5273
  "",
@@ -4115,10 +5277,13 @@ const HELP_TOPICS = new Map([
4115
5277
 
4116
5278
  ["icps list", [
4117
5279
  "Usage:",
4118
- " audienti icps list [--json] [--account <acct_id>]",
5280
+ " audienti icps list [--tag <tag>] [--json] [--account <acct_id>]",
4119
5281
  "",
4120
5282
  "Status: implemented",
4121
5283
  "",
5284
+ "Options:",
5285
+ " --tag <tag> Filter locally to ICPs whose tags include the normalized tag",
5286
+ "",
4122
5287
  "API:",
4123
5288
  " GET /api/v1/accounts/:account_id/icps.json",
4124
5289
  "",
@@ -4127,91 +5292,376 @@ const HELP_TOPICS = new Map([
4127
5292
  " prefix_id: icpp_",
4128
5293
  " name: string",
4129
5294
  " notes: string | null",
5295
+ " tags: [string]",
4130
5296
  " discovery_keyword: string | null",
4131
5297
  " agent: { id, name } | null"
4132
5298
  ].join("\n")],
4133
5299
 
4134
- ["icps create", [
5300
+ ["icps create", [
5301
+ "Usage:",
5302
+ " audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] | --payload <file.json>) [--json] [--account <acct_id>]",
5303
+ "",
5304
+ "Status: implemented",
5305
+ "",
5306
+ "Purpose:",
5307
+ " Create a new account ICP that can be attached to a motion or reused for targeting work.",
5308
+ "",
5309
+ "Input shape:",
5310
+ " name: string Required ICP name",
5311
+ " notes: string | optional",
5312
+ " tags: comma-separated tag list | optional",
5313
+ " discovery_keyword: string | optional",
5314
+ " payload: file.json | optional full ICP object using the account API create shape",
5315
+ "",
5316
+ "API:",
5317
+ " POST /api/v1/accounts/:account_id/icps.json",
5318
+ "",
5319
+ "Simple JSON body:",
5320
+ " {",
5321
+ " \"icp\": {",
5322
+ " \"name\": \"Renewal-stage IT leaders\",",
5323
+ " \"notes\": \"IT leaders reviewing vendors before renewal or QBR.\",",
5324
+ " \"discovery_keyword\": \"renewal\",",
5325
+ " \"tags\": [\"enterprise\", \"renewal\"]",
5326
+ " }",
5327
+ " }",
5328
+ "",
5329
+ "Payload file example:",
5330
+ " {",
5331
+ " \"name\": \"Vendor Management Office\",",
5332
+ " \"text_criteria\": \"Owns vendor governance, renewals, and escalations.\",",
5333
+ " \"discovery_keyword\": \"vendor governance\",",
5334
+ " \"negative_title_exceptions\": [\"sales\", \"recruiting\"],",
5335
+ " \"company_keywords\": {",
5336
+ " \"include\": [\"vendor governance\", \"supplier performance\"],",
5337
+ " \"exclude\": [\"staffing\"]",
5338
+ " },",
5339
+ " \"job_titles_attributes\": [",
5340
+ " {\"name\": \"Vendor Management Office\"},",
5341
+ " {\"name\": \"Strategic Vendor Management\"}",
5342
+ " ]",
5343
+ " }"
5344
+ ].join("\n")],
5345
+
5346
+ ["icps show", [
5347
+ "Usage:",
5348
+ ` ${ICPS_SHOW_USAGE.slice("Usage: ".length)}`,
5349
+ "",
5350
+ "Status: implemented",
5351
+ "",
5352
+ "Input shape:",
5353
+ " icp_id: icpp_ prefixed id or integer id",
5354
+ "",
5355
+ "API:",
5356
+ " GET /api/v1/accounts/:account_id/icps/:id.json"
5357
+ ].join("\n")],
5358
+
5359
+ ["icps update", [
5360
+ "Usage:",
5361
+ " audienti icps update <icp_id> [--name <text>] [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]",
5362
+ "",
5363
+ "Status: implemented",
5364
+ "",
5365
+ "Purpose:",
5366
+ " Update simple ICP fields or replace the ICP's full tag set.",
5367
+ "",
5368
+ "Input shape:",
5369
+ " icp_id: icpp_ prefixed id or integer id",
5370
+ " name: string | optional",
5371
+ " notes: string | optional",
5372
+ " discovery_keyword: string | optional",
5373
+ " tags: comma-separated tag list | optional",
5374
+ "",
5375
+ "API:",
5376
+ " PATCH /api/v1/accounts/:account_id/icps/:id.json",
5377
+ "",
5378
+ "JSON body:",
5379
+ " {",
5380
+ " \"icp\": {",
5381
+ " \"tags\": [\"enterprise\", \"renewal\"]",
5382
+ " }",
5383
+ " }"
5384
+ ].join("\n")],
5385
+
5386
+ ["icps add-tag", [
5387
+ "Usage:",
5388
+ " audienti icps add-tag <icp_id> <tag> [--json] [--account <acct_id>]",
5389
+ "",
5390
+ "Status: implemented",
5391
+ "",
5392
+ "Purpose:",
5393
+ " Add one normalized tag to an ICP.",
5394
+ "",
5395
+ "Input shape:",
5396
+ " icp_id: icpp_ prefixed id or integer id",
5397
+ " tag: string",
5398
+ "",
5399
+ "API:",
5400
+ " POST /api/v1/accounts/:account_id/icps/:id/add_tag.json",
5401
+ "",
5402
+ "JSON body:",
5403
+ " {",
5404
+ " \"tag\": \"enterprise\"",
5405
+ " }"
5406
+ ].join("\n")],
5407
+
5408
+ ["icps remove-tag", [
5409
+ "Usage:",
5410
+ " audienti icps remove-tag <icp_id> <tag> [--json] [--account <acct_id>]",
5411
+ "",
5412
+ "Status: implemented",
5413
+ "",
5414
+ "Purpose:",
5415
+ " Remove one normalized tag from an ICP.",
5416
+ "",
5417
+ "Input shape:",
5418
+ " icp_id: icpp_ prefixed id or integer id",
5419
+ " tag: string",
5420
+ "",
5421
+ "API:",
5422
+ " DELETE /api/v1/accounts/:account_id/icps/:id/remove_tag.json",
5423
+ "",
5424
+ "JSON body:",
5425
+ " {",
5426
+ " \"tag\": \"enterprise\"",
5427
+ " }"
5428
+ ].join("\n")],
5429
+
5430
+ ["companies", [
5431
+ "Usage:",
5432
+ " audienti companies search --query <text> [--json]",
5433
+ "",
5434
+ "Status: implemented",
5435
+ "",
5436
+ "Purpose:",
5437
+ " Returns persisted LinkedIn company profiles that match a company search query."
5438
+ ].join("\n")],
5439
+
5440
+ ["companies search", [
5441
+ "Usage:",
5442
+ " audienti companies search --query <text> [--json] [--account <acct_id>]",
5443
+ "",
5444
+ "Status: implemented",
5445
+ "",
5446
+ "API:",
5447
+ " GET /api/v1/accounts/:account_id/companies.json",
5448
+ "",
5449
+ "Input shape:",
5450
+ " query: string",
5451
+ "",
5452
+ "Output shape:",
5453
+ " companies[].prefix_id: prof_ profile id used by --company-profile",
5454
+ " companies[].citation_id: linkedin/company:...",
5455
+ " companies[].display_name: string",
5456
+ " companies[].url: string",
5457
+ " companies[].industry: string | null",
5458
+ " companies[].location: string | null"
5459
+ ].join("\n")],
5460
+
5461
+ ["dnc", [
5462
+ "Usage:",
5463
+ " audienti dnc list [--json]",
5464
+ ` ${DNC_ADD_USAGE.slice("Usage: ".length)}`,
5465
+ ` ${DNC_IMPORT_USAGE.slice("Usage: ".length)}`,
5466
+ ` ${DNC_REMOVE_USAGE.slice("Usage: ".length)}`,
5467
+ "",
5468
+ "Status: implemented",
5469
+ "",
5470
+ "Purpose:",
5471
+ " Manage account-level do-not-contact entries through the same settings/API path as the app."
5472
+ ].join("\n")],
5473
+
5474
+ ["dnc list", [
5475
+ "Usage:",
5476
+ " audienti dnc list [--limit <n>] [--offset <n>] [--json] [--account <acct_id>]",
5477
+ "",
5478
+ "Status: implemented",
5479
+ "",
5480
+ "API:",
5481
+ " GET /api/v1/accounts/:account_id/dnc.json"
5482
+ ].join("\n")],
5483
+
5484
+ ["dnc add", [
5485
+ "Usage:",
5486
+ ` ${DNC_ADD_USAGE.slice("Usage: ".length)}`,
5487
+ "",
5488
+ "Status: implemented",
5489
+ "",
5490
+ "Input shape:",
5491
+ " value: email, citation ID, or supported person profile URL",
5492
+ "",
5493
+ "Behavior:",
5494
+ " Creates or reactivates an account DNC entry and retroactively rejects matching account prospects.",
5495
+ "",
5496
+ "API:",
5497
+ " POST /api/v1/accounts/:account_id/dnc.json"
5498
+ ].join("\n")],
5499
+
5500
+ ["dnc import", [
5501
+ "Usage:",
5502
+ ` ${DNC_IMPORT_USAGE.slice("Usage: ".length)}`,
5503
+ "",
5504
+ "Status: implemented",
5505
+ "",
5506
+ "Input shape:",
5507
+ " file: text or CSV file. The CLI sends one value per line using column one for CSV-like rows.",
5508
+ "",
5509
+ "API:",
5510
+ " POST /api/v1/accounts/:account_id/dnc/import.json"
5511
+ ].join("\n")],
5512
+
5513
+ ["dnc remove", [
5514
+ "Usage:",
5515
+ ` ${DNC_REMOVE_USAGE.slice("Usage: ".length)}`,
5516
+ "",
5517
+ "Status: implemented",
5518
+ "",
5519
+ "API:",
5520
+ " DELETE /api/v1/accounts/:account_id/dnc/:id.json"
5521
+ ].join("\n")],
5522
+
5523
+ ["company-rules", [
5524
+ "Usage:",
5525
+ " audienti company-rules list [--json]",
5526
+ ` ${COMPANY_RULES_CREATE_USAGE.slice("Usage: ".length)}`,
5527
+ ` ${COMPANY_RULES_UPDATE_USAGE.slice("Usage: ".length)}`,
5528
+ ` ${COMPANY_RULES_REMOVE_USAGE.slice("Usage: ".length)}`,
5529
+ ` ${COMPANY_RULES_APPLY_USAGE.slice("Usage: ".length)}`,
5530
+ "",
5531
+ "Status: implemented",
5532
+ "",
5533
+ "Purpose:",
5534
+ " Manage account-wide and user-scoped company disposition rules keyed by LinkedIn company URL or domain."
5535
+ ].join("\n")],
5536
+
5537
+ ["company-rules list", [
5538
+ "Usage:",
5539
+ " audienti company-rules list [--json] [--account <acct_id>]",
5540
+ "",
5541
+ "Status: implemented",
5542
+ "",
5543
+ "API:",
5544
+ " GET /api/v1/accounts/:account_id/company_rules.json"
5545
+ ].join("\n")],
5546
+
5547
+ ["company-rules create", [
5548
+ "Usage:",
5549
+ ` ${COMPANY_RULES_CREATE_USAGE.slice("Usage: ".length)}`,
5550
+ "",
5551
+ "Status: implemented",
5552
+ "",
5553
+ "Input shape:",
5554
+ " linkedin-url: LinkedIn company URL | optional when domain is present",
5555
+ " domain: company domain | optional when linkedin-url is present",
5556
+ " disposition: monitor | nurture | not_fit | reject",
5557
+ " user: account user id, email, or me | optional. Omit for account-wide.",
5558
+ "",
5559
+ "Behavior:",
5560
+ " Matching prospects are always created first. Rule application then applies the disposition; monitor locks activity with lock_kind=company_policy until dedicated monitor mode exists.",
5561
+ "",
5562
+ "API:",
5563
+ " POST /api/v1/accounts/:account_id/company_rules.json"
5564
+ ].join("\n")],
5565
+
5566
+ ["company-rules update", [
5567
+ "Usage:",
5568
+ ` ${COMPANY_RULES_UPDATE_USAGE.slice("Usage: ".length)}`,
5569
+ "",
5570
+ "Status: implemented",
5571
+ "",
5572
+ "Input shape:",
5573
+ " user: account user id, email, me, or none. none makes the rule account-wide.",
5574
+ "",
5575
+ "API:",
5576
+ " PATCH /api/v1/accounts/:account_id/company_rules/:id.json"
5577
+ ].join("\n")],
5578
+
5579
+ ["company-rules remove", [
5580
+ "Usage:",
5581
+ ` ${COMPANY_RULES_REMOVE_USAGE.slice("Usage: ".length)}`,
5582
+ "",
5583
+ "Status: implemented",
5584
+ "",
5585
+ "API:",
5586
+ " DELETE /api/v1/accounts/:account_id/company_rules/:id.json"
5587
+ ].join("\n")],
5588
+
5589
+ ["company-rules apply", [
4135
5590
  "Usage:",
4136
- " audienti icps create (--name <text> [--notes <text>] [--discovery-keyword <text>] | --payload <file.json>) [--json] [--account <acct_id>]",
5591
+ ` ${COMPANY_RULES_APPLY_USAGE.slice("Usage: ".length)}`,
4137
5592
  "",
4138
5593
  "Status: implemented",
4139
5594
  "",
4140
- "Purpose:",
4141
- " Create a new account ICP that can be attached to a motion or reused for targeting work.",
4142
- "",
4143
- "Input shape:",
4144
- " name: string Required ICP name",
4145
- " notes: string | optional",
4146
- " discovery_keyword: string | optional",
4147
- " payload: file.json | optional full ICP object using the account API create shape",
5595
+ "Behavior:",
5596
+ " Backfills active visible account prospects through the same company-rule applicator used after profile writes.",
4148
5597
  "",
4149
5598
  "API:",
4150
- " POST /api/v1/accounts/:account_id/icps.json",
5599
+ " POST /api/v1/accounts/:account_id/company_rules/:id/apply.json",
5600
+ " POST /api/v1/accounts/:account_id/company_rules/apply_all.json"
5601
+ ].join("\n")],
5602
+
5603
+ ["tags", [
5604
+ "Usage:",
5605
+ " audienti tags list [--json]",
5606
+ " audienti tags show <tag> [--json]",
4151
5607
  "",
4152
- "Simple JSON body:",
4153
- " {",
4154
- " \"icp\": {",
4155
- " \"name\": \"Renewal-stage IT leaders\",",
4156
- " \"notes\": \"IT leaders reviewing vendors before renewal or QBR.\",",
4157
- " \"discovery_keyword\": \"renewal\"",
4158
- " }",
4159
- " }",
5608
+ "Status: implemented",
4160
5609
  "",
4161
- "Payload file example:",
4162
- " {",
4163
- " \"name\": \"Vendor Management Office\",",
4164
- " \"text_criteria\": \"Owns vendor governance, renewals, and escalations.\",",
4165
- " \"discovery_keyword\": \"vendor governance\",",
4166
- " \"negative_title_exceptions\": [\"sales\", \"recruiting\"],",
4167
- " \"company_keywords\": {",
4168
- " \"include\": [\"vendor governance\", \"supplier performance\"],",
4169
- " \"exclude\": [\"staffing\"]",
4170
- " },",
4171
- " \"job_titles_attributes\": [",
4172
- " {\"name\": \"Vendor Management Office\"},",
4173
- " {\"name\": \"Strategic Vendor Management\"}",
4174
- " ]",
4175
- " }"
5610
+ "Purpose:",
5611
+ " Show the shared vocabulary from ICP tags, list tags, and motion play_tags currently in use.",
5612
+ "",
5613
+ "Run `audienti tags list help` or `audienti tags show help` for output shape."
4176
5614
  ].join("\n")],
4177
5615
 
4178
- ["companies", [
5616
+ ["tags list", [
4179
5617
  "Usage:",
4180
- " audienti companies search --query <text> [--json]",
5618
+ " audienti tags list [--json] [--account <acct_id>]",
4181
5619
  "",
4182
5620
  "Status: implemented",
4183
5621
  "",
4184
5622
  "Purpose:",
4185
- " Returns persisted LinkedIn company profiles that match a company search query."
5623
+ " List normalized tags currently used by account ICPs, lists, and motions so new records can match existing labels.",
5624
+ "",
5625
+ "API:",
5626
+ " GET /api/v1/accounts/:account_id/tags.json",
5627
+ "",
5628
+ "Output shape:",
5629
+ " tags[].name: normalized tag string",
5630
+ " tags[].icp_count: number of ICPs using the tag",
5631
+ " tags[].list_count: number of lists using the tag",
5632
+ " tags[].motion_count: number of motions using the tag",
5633
+ " tags[].total_count: icp_count + list_count + motion_count"
4186
5634
  ].join("\n")],
4187
5635
 
4188
- ["companies search", [
5636
+ ["tags show", [
4189
5637
  "Usage:",
4190
- " audienti companies search --query <text> [--json] [--account <acct_id>]",
5638
+ " audienti tags show <tag> [--json] [--account <acct_id>]",
4191
5639
  "",
4192
5640
  "Status: implemented",
4193
5641
  "",
4194
- "API:",
4195
- " GET /api/v1/accounts/:account_id/companies.json",
5642
+ "Purpose:",
5643
+ " Show the ICPs, lists, and motions currently using one normalized tag.",
4196
5644
  "",
4197
- "Input shape:",
4198
- " query: string",
5645
+ "API:",
5646
+ " GET /api/v1/accounts/:account_id/icps.json",
5647
+ " GET /api/v1/accounts/:account_id/lists.json",
5648
+ " GET /api/v1/accounts/:account_id/motions.json",
4199
5649
  "",
4200
5650
  "Output shape:",
4201
- " companies[].prefix_id: prof_ profile id used by --company-profile",
4202
- " companies[].citation_id: linkedin/company:...",
4203
- " companies[].display_name: string",
4204
- " companies[].url: string",
4205
- " companies[].industry: string | null",
4206
- " companies[].location: string | null"
5651
+ " tag: normalized tag string",
5652
+ " icps[]: ICP rows whose tags include tag",
5653
+ " lists[]: list rows whose tags include tag",
5654
+ " motions[]: motion rows whose play_tags include tag"
4207
5655
  ].join("\n")],
4208
5656
 
4209
5657
  ["lists", [
4210
5658
  "Usage:",
4211
- " audienti lists list [--json]",
4212
- " audienti lists create --name <text> [--json]",
5659
+ " audienti lists list [--tag <tag>] [--json]",
5660
+ " audienti lists create --name <text> [--tags <tag[,tag...]>] [--json]",
4213
5661
  " audienti lists show <list_id> [--json]",
4214
- " audienti lists update <list_id> [--json]",
5662
+ " audienti lists update <list_id> [--tags <tag[,tag...]>] [--json]",
5663
+ " audienti lists add-tag <list_id> <tag> [--json]",
5664
+ " audienti lists remove-tag <list_id> <tag> [--json]",
4215
5665
  " audienti lists delete <list_id> --confirm <yes|true|Y|y> [--json]",
4216
5666
  " audienti lists prospects <list_id> [--json]",
4217
5667
  " audienti lists add-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
@@ -4225,10 +5675,13 @@ const HELP_TOPICS = new Map([
4225
5675
 
4226
5676
  ["lists list", [
4227
5677
  "Usage:",
4228
- " audienti lists list [--json] [--account <acct_id>]",
5678
+ " audienti lists list [--tag <tag>] [--json] [--account <acct_id>]",
4229
5679
  "",
4230
5680
  "Status: implemented",
4231
5681
  "",
5682
+ "Options:",
5683
+ " --tag <tag> Filter locally to lists whose tags include the normalized tag",
5684
+ "",
4232
5685
  "API:",
4233
5686
  " GET /api/v1/accounts/:account_id/lists.json",
4234
5687
  "",
@@ -4239,12 +5692,13 @@ const HELP_TOPICS = new Map([
4239
5692
  " description: string | null",
4240
5693
  " prospect_count: integer",
4241
5694
  " protected_system_list: boolean",
4242
- " hubspot_synced: boolean"
5695
+ " hubspot_synced: boolean",
5696
+ " tags: [string]"
4243
5697
  ].join("\n")],
4244
5698
 
4245
5699
  ["lists create", [
4246
5700
  "Usage:",
4247
- " audienti lists create --name <text> [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
5701
+ " audienti lists create --name <text> [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
4248
5702
  "",
4249
5703
  "Status: implemented",
4250
5704
  "",
@@ -4254,6 +5708,7 @@ const HELP_TOPICS = new Map([
4254
5708
  "Input shape:",
4255
5709
  " name: string Required list name",
4256
5710
  " description: string | optional",
5711
+ " tags: comma-separated tag list | optional",
4257
5712
  " campaign_hook: string | optional",
4258
5713
  " audience_note: string | optional",
4259
5714
  "",
@@ -4262,10 +5717,11 @@ const HELP_TOPICS = new Map([
4262
5717
  "",
4263
5718
  "JSON body:",
4264
5719
  " {",
4265
- " \"list\": {",
4266
- " \"name\": \"CIO renewal targets\",",
4267
- " \"description\": \"Accounts to review before QBR outreach.\",",
4268
- " \"campaign_brief\": {",
5720
+ " \"list\": {",
5721
+ " \"name\": \"CIO renewal targets\",",
5722
+ " \"description\": \"Accounts to review before QBR outreach.\",",
5723
+ " \"tags\": [\"sarit\", \"pj\"],",
5724
+ " \"campaign_brief\": {",
4269
5725
  " \"hook\": \"Vendor accountability before renewal\",",
4270
5726
  " \"audience_note\": \"IT leaders running QBRs and renewals\"",
4271
5727
  " }",
@@ -4288,7 +5744,7 @@ const HELP_TOPICS = new Map([
4288
5744
 
4289
5745
  ["lists update", [
4290
5746
  "Usage:",
4291
- " audienti lists update <list_id> [--name <text>] [--description <text>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
5747
+ " audienti lists update <list_id> [--name <text>] [--description <text>] [--tags <tag[,tag...]>] [--campaign-hook <text>] [--audience-note <text>] [--json] [--account <acct_id>]",
4292
5748
  "",
4293
5749
  "Status: implemented",
4294
5750
  "",
@@ -4299,6 +5755,7 @@ const HELP_TOPICS = new Map([
4299
5755
  " list_id: list_ prefix id",
4300
5756
  " list.name: string | optional",
4301
5757
  " list.description: string | optional",
5758
+ " list.tags: comma-separated tag list | optional",
4302
5759
  " list.campaign_brief.hook: string | optional",
4303
5760
  " list.campaign_brief.audience_note: string | optional",
4304
5761
  "",
@@ -4306,6 +5763,50 @@ const HELP_TOPICS = new Map([
4306
5763
  " PATCH /api/v1/accounts/:account_id/lists/:id.json"
4307
5764
  ].join("\n")],
4308
5765
 
5766
+ ["lists add-tag", [
5767
+ "Usage:",
5768
+ ` ${LISTS_ADD_TAG_USAGE.slice("Usage: ".length)}`,
5769
+ "",
5770
+ "Status: implemented",
5771
+ "",
5772
+ "Purpose:",
5773
+ " Add one normalized tag to a list without changing prospect membership.",
5774
+ "",
5775
+ "Input shape:",
5776
+ " list_id: list_ prefix id",
5777
+ " tag: string",
5778
+ "",
5779
+ "API:",
5780
+ " POST /api/v1/accounts/:account_id/lists/:id/add_tag.json",
5781
+ "",
5782
+ "JSON body:",
5783
+ " {",
5784
+ " \"tag\": \"sarit\"",
5785
+ " }"
5786
+ ].join("\n")],
5787
+
5788
+ ["lists remove-tag", [
5789
+ "Usage:",
5790
+ ` ${LISTS_REMOVE_TAG_USAGE.slice("Usage: ".length)}`,
5791
+ "",
5792
+ "Status: implemented",
5793
+ "",
5794
+ "Purpose:",
5795
+ " Remove one normalized tag from a list.",
5796
+ "",
5797
+ "Input shape:",
5798
+ " list_id: list_ prefix id",
5799
+ " tag: string",
5800
+ "",
5801
+ "API:",
5802
+ " DELETE /api/v1/accounts/:account_id/lists/:id/remove_tag.json",
5803
+ "",
5804
+ "JSON body:",
5805
+ " {",
5806
+ " \"tag\": \"sarit\"",
5807
+ " }"
5808
+ ].join("\n")],
5809
+
4309
5810
  ["lists delete", [
4310
5811
  "Usage:",
4311
5812
  " audienti lists delete <list_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]",
@@ -4398,14 +5899,17 @@ const HELP_TOPICS = new Map([
4398
5899
 
4399
5900
  ["motions", [
4400
5901
  "Usage:",
4401
- " audienti motions list [--json]",
5902
+ " audienti motions list [--tag <tag>] [--json]",
4402
5903
  " audienti motions show <motn_id> [--json]",
4403
5904
  " audienti motions status <motn_id> [--json]",
5905
+ " audienti motions run-discovery <motn_id> [--target-count <n>] [--json]",
4404
5906
  " audienti motions analytics <motn_id> [--window 30d] [--json]",
4405
5907
  " audienti motions prospects <motn_id> [--json]",
4406
5908
  " audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
4407
5909
  " audienti motions create --payload <file.json> [--json]",
4408
- " audienti motions update <motn_id> --status <draft|preparing|active|paused|archived> [--json]",
5910
+ " audienti motions update <motn_id> [--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--json]",
5911
+ " audienti motions add-tag <motn_id> <tag> [--json]",
5912
+ " audienti motions remove-tag <motn_id> <tag> [--json]",
4409
5913
  " audienti motions activate <motn_id> [--json]",
4410
5914
  " audienti motions pause <motn_id> [--json]",
4411
5915
  " audienti motions archive <motn_id> [--json]",
@@ -4413,7 +5917,7 @@ const HELP_TOPICS = new Map([
4413
5917
  " audienti motions clone <motn_id> --name <text> [--json]",
4414
5918
  " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...] [--json]",
4415
5919
  "",
4416
- "Status: read, create, status update, delete, clone, status, and prospect attachment commands implemented",
5920
+ "Status: read, create, status update, delete, clone, status, discovery launch, and prospect attachment commands implemented",
4417
5921
  "",
4418
5922
  "CLI synonym:",
4419
5923
  " `plays` is accepted anywhere `motions` is accepted",
@@ -4424,10 +5928,13 @@ const HELP_TOPICS = new Map([
4424
5928
 
4425
5929
  ["motions list", [
4426
5930
  "Usage:",
4427
- " audienti motions list [--json] [--account <acct_id>]",
5931
+ " audienti motions list [--tag <tag>] [--json] [--account <acct_id>]",
4428
5932
  "",
4429
5933
  "Status: implemented",
4430
5934
  "",
5935
+ "Options:",
5936
+ " --tag <tag> Filter locally to motions whose play_tags include the normalized tag",
5937
+ "",
4431
5938
  "API:",
4432
5939
  " GET /api/v1/accounts/:account_id/motions.json",
4433
5940
  "",
@@ -4440,7 +5947,8 @@ const HELP_TOPICS = new Map([
4440
5947
  " offer.prefix_id: offr_",
4441
5948
  " icp.prefix_id: icpp_",
4442
5949
  " list.prefix_id: list_ | null",
4443
- " principal_account_user.id: integer"
5950
+ " principal_account_user.id: integer",
5951
+ " play_tags: [string]"
4444
5952
  ].join("\n")],
4445
5953
 
4446
5954
  ["motions show", [
@@ -4498,6 +6006,27 @@ const HELP_TOPICS = new Map([
4498
6006
  " GET /api/v1/accounts/:account_id/analytics/prospects.json?motion_id=:motion_id"
4499
6007
  ].join("\n")],
4500
6008
 
6009
+ ["motions run-discovery", [
6010
+ "Usage:",
6011
+ ` ${MOTIONS_RUN_DISCOVERY_USAGE.slice("Usage: ".length)}`,
6012
+ "",
6013
+ "Status: implemented",
6014
+ "",
6015
+ "Purpose:",
6016
+ " Queue an immediate discovery run for one discovery-capable motion or play.",
6017
+ "",
6018
+ "Options:",
6019
+ " --target-count <n> Override the manual replenishment target count for this launch.",
6020
+ "",
6021
+ "Output shape:",
6022
+ " enqueued: true when Motions::DiscoverJob was queued",
6023
+ " reason: launched | run_in_progress | lock_contention | target_met | enqueue_failed",
6024
+ " target_count: requested target count",
6025
+ "",
6026
+ "API:",
6027
+ " POST /api/v1/accounts/:account_id/motions/:id/run_discovery.json"
6028
+ ].join("\n")],
6029
+
4501
6030
  ["motions prospects", [
4502
6031
  "Usage:",
4503
6032
  " audienti motions prospects <motn_id> [--json] [--account <acct_id>]",
@@ -4568,7 +6097,8 @@ const HELP_TOPICS = new Map([
4568
6097
  " principal_account_user_id: integer | me | optional",
4569
6098
  " icp_id: icpp_ prefix id | optional",
4570
6099
  " list_id: list_ prefix id | optional",
4571
- " inbound_channels: [linkedin | reddit | x | tiktok | instagram | facebook] | optional",
6100
+ " play_tags: [string] | optional",
6101
+ " inbound_channels: [linkedin | reddit] | optional",
4572
6102
  " lopa_profiles: [{ url: string, source_type: creator | competitor | partner | customer | other }] | optional",
4573
6103
  "",
4574
6104
  "JSON example:",
@@ -4579,7 +6109,8 @@ const HELP_TOPICS = new Map([
4579
6109
  " \"status\": \"draft\",",
4580
6110
  " \"offer_id\": \"offr_abc123\",",
4581
6111
  " \"principal_account_user_id\": 42,",
4582
- " \"list_id\": \"list_abc123\"",
6112
+ " \"list_id\": \"list_abc123\",",
6113
+ " \"play_tags\": [\"sarit\", \"pj\"]",
4583
6114
  " }",
4584
6115
  "",
4585
6116
  "Behavior:",
@@ -4622,23 +6153,69 @@ const HELP_TOPICS = new Map([
4622
6153
  "Status: implemented",
4623
6154
  "",
4624
6155
  "Purpose:",
4625
- " Change one motion or play's lifecycle status.",
6156
+ " Change one motion or play's lifecycle status or replace its tag set.",
4626
6157
  "",
4627
6158
  "Input shape:",
4628
6159
  " motn_id: motn_ prefix id",
4629
- " status: draft | preparing | active | paused | archived",
6160
+ " status: draft | preparing | active | paused | archived | optional",
6161
+ " tags: comma-separated tag list | optional",
4630
6162
  "",
4631
6163
  "Behavior:",
4632
- " Updates only the motion status. Other motion configuration stays unchanged.",
6164
+ " Updates only the provided fields. Sending --tags replaces the motion's full tag set.",
4633
6165
  "",
4634
6166
  "API:",
4635
6167
  " PATCH /api/v1/accounts/:account_id/motions/:id.json",
4636
6168
  "",
4637
6169
  "JSON body:",
4638
6170
  " {",
4639
- " \"motion\": {",
4640
- " \"status\": \"paused\"",
4641
- " }",
6171
+ " \"motion\": {",
6172
+ " \"status\": \"paused\",",
6173
+ " \"play_tags\": [\"sarit\", \"pj\"]",
6174
+ " }",
6175
+ " }"
6176
+ ].join("\n")],
6177
+
6178
+ ["motions add-tag", [
6179
+ "Usage:",
6180
+ ` ${MOTIONS_ADD_TAG_USAGE.slice("Usage: ".length)}`,
6181
+ "",
6182
+ "Status: implemented",
6183
+ "",
6184
+ "Purpose:",
6185
+ " Add one normalized tag to a motion or play.",
6186
+ "",
6187
+ "Input shape:",
6188
+ " motn_id: motn_ prefix id",
6189
+ " tag: string",
6190
+ "",
6191
+ "API:",
6192
+ " POST /api/v1/accounts/:account_id/motions/:id/add_tag.json",
6193
+ "",
6194
+ "JSON body:",
6195
+ " {",
6196
+ " \"tag\": \"sarit\"",
6197
+ " }"
6198
+ ].join("\n")],
6199
+
6200
+ ["motions remove-tag", [
6201
+ "Usage:",
6202
+ ` ${MOTIONS_REMOVE_TAG_USAGE.slice("Usage: ".length)}`,
6203
+ "",
6204
+ "Status: implemented",
6205
+ "",
6206
+ "Purpose:",
6207
+ " Remove one normalized tag from a motion or play.",
6208
+ "",
6209
+ "Input shape:",
6210
+ " motn_id: motn_ prefix id",
6211
+ " tag: string",
6212
+ "",
6213
+ "API:",
6214
+ " DELETE /api/v1/accounts/:account_id/motions/:id/remove_tag.json",
6215
+ "",
6216
+ "JSON body:",
6217
+ " {",
6218
+ " \"tag\": \"sarit\"",
4642
6219
  " }"
4643
6220
  ].join("\n")],
4644
6221
 
@@ -4732,8 +6309,15 @@ const HELP_TOPICS = new Map([
4732
6309
  ["prospects", [
4733
6310
  "Usage:",
4734
6311
  " audienti prospects list [--json] [filters]",
6312
+ " audienti prospects check [--json|--csv] [filters]",
4735
6313
  " audienti prospects show <prsp_id> [--json]",
4736
6314
  " audienti prospects assign <prsp_id> [prsp_id...] --assigned-user <id|me|unassign> [--json]",
6315
+ " audienti prospects set-status <prsp_id> --status <active|nurture|non_responsive|not_fit|rejected> [--json]",
6316
+ " audienti prospects reject <prsp_id> [--json]",
6317
+ " audienti prospects nurture <prsp_id> [--reason <reason>] [--json]",
6318
+ " audienti prospects restore <prsp_id> [--json]",
6319
+ " audienti prospects lock <prsp_id> [--note <text>] [--json]",
6320
+ " audienti prospects unlock <prsp_id> [--json]",
4737
6321
  " audienti prospects timeline <prsp_id> [--json]",
4738
6322
  " audienti prospects message-types <prsp_id> [--json]",
4739
6323
  " audienti prospects write <prsp_id> --type <surface_key> [--json]",
@@ -4747,7 +6331,7 @@ const HELP_TOPICS = new Map([
4747
6331
  " audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--json]",
4748
6332
  " audienti prospects import-status <primp_id> [--json]",
4749
6333
  "",
4750
- "Status: read commands, assignment, per-prospect draft preview, sequence preview, and import implemented; disposition planned",
6334
+ "Status: read commands, assignment, disposition, lock/unlock, per-prospect draft preview, sequence preview, and import implemented",
4751
6335
  "",
4752
6336
  "Filters:",
4753
6337
  " --query <text>",
@@ -4816,6 +6400,43 @@ const HELP_TOPICS = new Map([
4816
6400
  " audienti prospects list --all --csv"
4817
6401
  ].join("\n")],
4818
6402
 
6403
+ ["prospects check", [
6404
+ "Usage:",
6405
+ " audienti prospects check [--json|--csv] [filters] [--account <acct_id>]",
6406
+ "",
6407
+ "Status: implemented",
6408
+ "",
6409
+ "Description:",
6410
+ " Lists suspect people prospects that do not have a certified company employment citation.",
6411
+ " Imported or reported company text is shown for investigation but does not count as certification.",
6412
+ "",
6413
+ "Filters:",
6414
+ " --query <text> Search name, title, company, email, profile URL",
6415
+ " --motion <motn_id> Filter to a motion",
6416
+ " --play <motn_id> Filter to a play using the same motion relationship",
6417
+ " --list <list_id> Filter to a prospect list",
6418
+ " --stage <stage> Filter to a pipeline stage",
6419
+ " --assigned-user <id|me|unassigned> Filter by assigned account user",
6420
+ " --limit <n> Max rows for one page; with --all it caps total rows up to 1000",
6421
+ " --page <n> 1-based page number",
6422
+ " --offset <n> Row offset for manual pagination",
6423
+ " --all Fetch every matching suspect prospect up to 1000 rows",
6424
+ " --csv Export CSV with app_url for operator review",
6425
+ "",
6426
+ "Output shape:",
6427
+ " prospects[].company_certification.status: missing",
6428
+ " prospects[].company_certification.reason: missing_employment_citation",
6429
+ " prospects[].app_url: product URL for operator inspection",
6430
+ " meta.total_count: total matching suspect prospects",
6431
+ "",
6432
+ "API:",
6433
+ " GET /api/v1/accounts/:account_id/prospects.json?data_quality=missing_certified_company",
6434
+ "",
6435
+ "Examples:",
6436
+ " audienti prospects check --motion <motn_id> --all --csv",
6437
+ " audienti prospects check --assigned-user me --json"
6438
+ ].join("\n")],
6439
+
4819
6440
  ["prospects assign", [
4820
6441
  "Usage:",
4821
6442
  ` ${PROSPECTS_ASSIGN_USAGE.slice("Usage: ".length)}`,
@@ -4839,6 +6460,27 @@ const HELP_TOPICS = new Map([
4839
6460
  " }"
4840
6461
  ].join("\n")],
4841
6462
 
6463
+ ["prospects set-status", [
6464
+ "Usage:",
6465
+ ` ${PROSPECTS_SET_STATUS_USAGE.slice("Usage: ".length)}`,
6466
+ "",
6467
+ "Status: implemented",
6468
+ "",
6469
+ "Purpose:",
6470
+ " Set the account-scoped prospect disposition directly, without routing through a motion.",
6471
+ "",
6472
+ "Input shape:",
6473
+ " status: active | nurture | non_responsive | not_fit | rejected",
6474
+ "",
6475
+ "Behavior:",
6476
+ " active restores the prospect, rejected uses the rejection/DNC path, and nurture/non_responsive/not_fit use the shared inactive disposition path.",
6477
+ "",
6478
+ "API:",
6479
+ " POST /api/v1/accounts/:account_id/prospects/:id/restore.json",
6480
+ " POST /api/v1/accounts/:account_id/prospects/:id/reject.json",
6481
+ " POST /api/v1/accounts/:account_id/prospects/:id/nurture.json"
6482
+ ].join("\n")],
6483
+
4842
6484
  ["prospects show", [
4843
6485
  "Usage:",
4844
6486
  " audienti prospects show <prsp_id> [--json] [--account <acct_id>]",
@@ -4852,6 +6494,78 @@ const HELP_TOPICS = new Map([
4852
6494
  " GET /api/v1/accounts/:account_id/prospects/:id.json"
4853
6495
  ].join("\n")],
4854
6496
 
6497
+ ["prospects reject", [
6498
+ "Usage:",
6499
+ ` ${PROSPECTS_REJECT_USAGE.slice("Usage: ".length)}`,
6500
+ "",
6501
+ "Status: implemented",
6502
+ "",
6503
+ "Purpose:",
6504
+ " Reject one prospect through the shared disposition and account DNC path.",
6505
+ "",
6506
+ "API:",
6507
+ " POST /api/v1/accounts/:account_id/prospects/:id/reject.json"
6508
+ ].join("\n")],
6509
+
6510
+ ["prospects nurture", [
6511
+ "Usage:",
6512
+ ` ${PROSPECTS_NURTURE_USAGE.slice("Usage: ".length)}`,
6513
+ "",
6514
+ "Status: implemented",
6515
+ "",
6516
+ "Purpose:",
6517
+ " Move one prospect to an inactive nurture disposition through the shared disposition path.",
6518
+ "",
6519
+ "Input shape:",
6520
+ " reason: nurture | non_responsive | not_fit. Defaults to nurture.",
6521
+ "",
6522
+ "API:",
6523
+ " POST /api/v1/accounts/:account_id/prospects/:id/nurture.json"
6524
+ ].join("\n")],
6525
+
6526
+ ["prospects restore", [
6527
+ "Usage:",
6528
+ ` ${PROSPECTS_RESTORE_USAGE.slice("Usage: ".length)}`,
6529
+ "",
6530
+ "Status: implemented",
6531
+ "",
6532
+ "Purpose:",
6533
+ " Restore one rejected or inactive prospect through the shared disposition path.",
6534
+ "",
6535
+ "API:",
6536
+ " POST /api/v1/accounts/:account_id/prospects/:id/restore.json"
6537
+ ].join("\n")],
6538
+
6539
+ ["prospects lock", [
6540
+ "Usage:",
6541
+ ` ${PROSPECTS_LOCK_USAGE.slice("Usage: ".length)}`,
6542
+ "",
6543
+ "Status: implemented",
6544
+ "",
6545
+ "Purpose:",
6546
+ " Lock one prospect immediately through the same protected-relationship path used by the prospect page.",
6547
+ "",
6548
+ "Input shape:",
6549
+ " kind: protected_relationship | company_policy. Defaults to protected_relationship.",
6550
+ " note: optional operator note explaining the lock.",
6551
+ "",
6552
+ "API:",
6553
+ " POST /api/v1/accounts/:account_id/prospects/:id/lock.json"
6554
+ ].join("\n")],
6555
+
6556
+ ["prospects unlock", [
6557
+ "Usage:",
6558
+ ` ${PROSPECTS_UNLOCK_USAGE.slice("Usage: ".length)}`,
6559
+ "",
6560
+ "Status: implemented",
6561
+ "",
6562
+ "Purpose:",
6563
+ " Clear one prospect lock through the same unlock path used by the prospect page.",
6564
+ "",
6565
+ "API:",
6566
+ " POST /api/v1/accounts/:account_id/prospects/:id/unlock.json"
6567
+ ].join("\n")],
6568
+
4855
6569
  ["prospects timeline", [
4856
6570
  "Usage:",
4857
6571
  " audienti prospects timeline <prsp_id> [--json] [--types <post,comment,reaction>] [--limit <n>] [--account <acct_id>]",
@@ -5209,26 +6923,6 @@ const HELP_TOPICS = new Map([
5209
6923
  " GET /api/v1/accounts/:account_id/prospect_imports/:id.json"
5210
6924
  ].join("\n")],
5211
6925
 
5212
- ["prospects disposition", [
5213
- "Usage:",
5214
- " audienti prospects disposition <prsp_id> --payload <file.json> [--account <acct_id>]",
5215
- "",
5216
- "Status: planned",
5217
- "",
5218
- "Input shape:",
5219
- " action: defer | nurture | reject | restore",
5220
- " reason: string | optional",
5221
- " delay_until: ISO8601 datetime | optional",
5222
- " note: string | optional",
5223
- "",
5224
- "JSON example:",
5225
- " {",
5226
- " \"action\": \"nurture\",",
5227
- " \"reason\": \"not_a_fit\",",
5228
- " \"note\": \"Not a fit for this motion right now.\"",
5229
- " }"
5230
- ].join("\n")],
5231
-
5232
6926
  ["tools", [
5233
6927
  "Usage:",
5234
6928
  " audienti tools get <email|phone> --url <linkedin_url> [--json]",
@@ -5358,6 +7052,7 @@ const HELP_TOPICS = new Map([
5358
7052
  ["analytics", [
5359
7053
  "Usage:",
5360
7054
  " 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]",
7055
+ " audienti analytics dashboard [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--json]",
5361
7056
  " audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--user <account_user_id|email|name|me>] [--json]",
5362
7057
  " 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]",
5363
7058
  " audienti analytics visibility [--window 24h] [--user <account_user_id|email|name|me>] [--json]",
@@ -5371,13 +7066,42 @@ const HELP_TOPICS = new Map([
5371
7066
  " --start <YYYY-MM-DD> --end <YYYY-MM-DD> For user analytics, select the events.created_at activity range instead of --window.",
5372
7067
  " --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.",
5373
7068
  " --motion <motn_id> For prospect and user analytics, filter AccountProspect.motion_id to one motion/play.",
7069
+ " --play-tag <tag> For dashboard analytics, filter to motions/lists tagged with a campaign tag.",
5374
7070
  " --provenance <source> Optional lower-level AccountProspect.intake_source filter.",
5375
7071
  " --platform <linkedin|email|gmail> For user analytics, filter events.platform. --channel is accepted as an alias.",
5376
7072
  " cohort-analysis loops over recent weekly AccountProspect.created_at cohorts and compares their current stages.",
5377
7073
  " --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.",
5378
7074
  "",
5379
7075
  "Output:",
5380
- " Account-scoped analytics for prospects, users, visibility engagement, and ContentOps publishing."
7076
+ " Account-scoped analytics for prospects, campaign dashboard counts, users, visibility engagement, and ContentOps publishing."
7077
+ ].join("\n")],
7078
+
7079
+ ["analytics dashboard", [
7080
+ "Usage:",
7081
+ ` ${ANALYTICS_DASHBOARD_USAGE.slice("Usage: ".length)}`,
7082
+ "",
7083
+ "Status: implemented",
7084
+ "",
7085
+ "Purpose:",
7086
+ " Return the dashboard outreach read model through the CLI, including distinct company target counts for a motion, tag, offer, ICP, or user-filtered cohort.",
7087
+ "",
7088
+ "Options:",
7089
+ " --cohort-start <YYYY-MM-DD> --cohort-end <YYYY-MM-DD> Select the AccountProspect.created_at cohort.",
7090
+ " --play-tag <tag> Filter to motions/lists tagged with a campaign tag. --tag is accepted as an alias.",
7091
+ " --motion <motn_id> Filter to one motion/play.",
7092
+ " --offer <offr_id> Filter to one offer.",
7093
+ " --icp <icp_id> Filter to one ICP.",
7094
+ " --user <account_user_id|email|name|me> Filter to prospects assigned to one account user.",
7095
+ "",
7096
+ "Output shape:",
7097
+ " cohort_size: people in the selected cohort",
7098
+ " cohort_company_target_count: distinct company targets in that cohort",
7099
+ " cohort_people_per_company_average: people per company target",
7100
+ " active_cohort_count and active_cohort_company_target_count: still-active campaign cohort counts",
7101
+ " pipeline_stage_counts[]: current stage distribution",
7102
+ "",
7103
+ "API:",
7104
+ " GET /api/v1/accounts/:account_id/analytics/dashboard.json"
5381
7105
  ].join("\n")],
5382
7106
 
5383
7107
  ["analytics prospects", [
@@ -5529,6 +7253,7 @@ const HELP_TOPICS = new Map([
5529
7253
  " audienti motions pause <motn_id>",
5530
7254
  " audienti motions delete <motn_id> --confirm yes",
5531
7255
  " audienti motions status <motn_id>",
7256
+ " audienti motions run-discovery <motn_id>",
5532
7257
  "",
5533
7258
  "3. Add a new prospect from LinkedIn and poll enrichment",
5534
7259
  " audienti lists create --name \"Target list\"",
@@ -5550,6 +7275,12 @@ const HELP_TOPICS = new Map([
5550
7275
  " audienti prospects add-profile <prsp_id> --url prospect@example.com",
5551
7276
  " audienti prospects report-bad-profile <prsp_id> <prof_id>",
5552
7277
  " audienti prospects add-note <prsp_id> --type steer --message \"Meeting will not happen\" --engagement-type action.meeting.canceled",
7278
+ " audienti prospects set-status <prsp_id> --status not_fit",
7279
+ " audienti prospects lock <prsp_id> --note \"Emergency hold\"",
7280
+ " audienti prospects reject <prsp_id>",
7281
+ " audienti prospects nurture <prsp_id>",
7282
+ " audienti prospects restore <prsp_id>",
7283
+ " audienti prospects unlock <prsp_id>",
5553
7284
  " audienti prospects sequence-preview <prsp_id>",
5554
7285
  " audienti writer test-run <prsp_id>",
5555
7286
  " audienti prospects sequence-export <prsp_id> --csv",
@@ -5567,6 +7298,7 @@ const HELP_TOPICS = new Map([
5567
7298
  "7. Inspect account analytics",
5568
7299
  " audienti users activity me --window 7d",
5569
7300
  " audienti analytics prospects --window 24h",
7301
+ " audienti analytics dashboard --play-tag wine_campaign",
5570
7302
  " audienti analytics users --user me --window 30d",
5571
7303
  " audienti analytics visibility --window 24h --user me",
5572
7304
  " audienti analytics content --window week",
@@ -5578,7 +7310,6 @@ const HELP_TOPICS = new Map([
5578
7310
  " Prefer prospects import for new LinkedIn people and add-prospects commands for records that already exist.",
5579
7311
  "",
5580
7312
  "Current gaps to plan around:",
5581
- " Prospect disposition still lacks a dedicated CLI mutation.",
5582
7313
  " Operator outcome writeback is implemented for prospect rows, not visibility rows."
5583
7314
  ].join("\n")]
5584
7315
  ]);