@audienti/cli 0.1.15 → 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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to the Audienti CLI are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.17] - 2026-07-17
8
+
9
+ ### Added
10
+
11
+ - Add `audienti dnc list/add/import/remove` for account DNC settings.
12
+ - Add `audienti company-rules list/create/update/remove/apply` for account-wide and user-scoped company disposition policies.
13
+
14
+ ## [0.1.16] - 2026-07-17
15
+
16
+ ### Added
17
+
18
+ - Add direct prospect emergency state commands: `audienti prospects set-status`, `audienti prospects lock`, and `audienti prospects unlock`.
19
+
7
20
  ## [0.1.15] - 2026-07-16
8
21
 
9
22
  ### Added
package/README.md CHANGED
@@ -70,6 +70,10 @@ audienti prospects list --profiles
70
70
  audienti prospects list --assigned-user unassigned
71
71
  audienti prospects check --all --csv
72
72
  audienti prospects assign <prsp_id> --assigned-user me
73
+ audienti prospects set-status <prsp_id> --status not_fit
74
+ audienti prospects lock <prsp_id> --note "Emergency hold"
75
+ audienti dnc list
76
+ audienti company-rules list
73
77
  audienti users activity --window 7d
74
78
  audienti analytics prospects --window 24h
75
79
  audienti analytics dashboard --play-tag wine_campaign
@@ -170,6 +174,25 @@ audienti prospects assign <prsp_id> --assigned-user <account_user_id|me>
170
174
  audienti prospects assign <prsp_id> --assigned-user unassign
171
175
  ```
172
176
 
177
+ To make emergency prospect state changes without going through a motion:
178
+
179
+ ```bash
180
+ audienti prospects set-status <prsp_id> --status nurture
181
+ audienti prospects set-status <prsp_id> --status not_fit
182
+ audienti prospects lock <prsp_id> --note "Emergency hold"
183
+ audienti prospects unlock <prsp_id>
184
+ ```
185
+
186
+ To manage account DNC and company disposition rules from the CLI:
187
+
188
+ ```bash
189
+ audienti dnc add prospect@example.com
190
+ audienti dnc import --file dnc.txt
191
+ audienti company-rules create --linkedin-url https://www.linkedin.com/company/example --disposition monitor
192
+ audienti company-rules create --domain example.com --disposition not_fit --user me
193
+ audienti company-rules apply --all
194
+ ```
195
+
173
196
  To import multiple LinkedIn people through the same per-prospect import path:
174
197
 
175
198
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,6 +53,9 @@ audienti help agent-workflows
53
53
  audienti prospects list --query "name or company" --wide --json
54
54
  audienti prospects list --assigned-user unassigned --json
55
55
  audienti prospects assign <prsp_id> --assigned-user me --json
56
+ audienti prospects set-status <prsp_id> --status not_fit --json
57
+ audienti prospects lock <prsp_id> --note "Emergency hold" --json
58
+ audienti prospects unlock <prsp_id> --json
56
59
  audienti users activity me --window 7d --json
57
60
  audienti prospects import-batch --file prospects.csv --motion <motn_id> --assigned-user me --json
58
61
  audienti lists create --name "Target list" --json
package/src/api-client.js CHANGED
@@ -115,6 +115,66 @@ export class AudientiClient {
115
115
  return this.requestJson(accountPath(accountId, ["companies"], query));
116
116
  }
117
117
 
118
+ dncEntries(accountId, query = {}) {
119
+ return this.requestJson(accountPath(accountId, ["dnc"], query));
120
+ }
121
+
122
+ createDncEntry(accountId, body) {
123
+ return this.requestJson(accountPath(accountId, ["dnc"]), {
124
+ method: "POST",
125
+ body
126
+ });
127
+ }
128
+
129
+ importDncEntries(accountId, body) {
130
+ return this.requestJson(accountPath(accountId, ["dnc", "import"]), {
131
+ method: "POST",
132
+ body
133
+ });
134
+ }
135
+
136
+ deleteDncEntry(accountId, entryId) {
137
+ return this.requestJson(accountPath(accountId, ["dnc", entryId]), {
138
+ method: "DELETE"
139
+ });
140
+ }
141
+
142
+ companyRules(accountId) {
143
+ return this.requestJson(accountPath(accountId, ["company_rules"]));
144
+ }
145
+
146
+ createCompanyRule(accountId, body) {
147
+ return this.requestJson(accountPath(accountId, ["company_rules"]), {
148
+ method: "POST",
149
+ body
150
+ });
151
+ }
152
+
153
+ updateCompanyRule(accountId, ruleId, body) {
154
+ return this.requestJson(accountPath(accountId, ["company_rules", ruleId]), {
155
+ method: "PATCH",
156
+ body
157
+ });
158
+ }
159
+
160
+ deleteCompanyRule(accountId, ruleId) {
161
+ return this.requestJson(accountPath(accountId, ["company_rules", ruleId]), {
162
+ method: "DELETE"
163
+ });
164
+ }
165
+
166
+ applyCompanyRule(accountId, ruleId) {
167
+ return this.requestJson(accountPath(accountId, ["company_rules", ruleId, "apply"]), {
168
+ method: "POST"
169
+ });
170
+ }
171
+
172
+ applyAllCompanyRules(accountId) {
173
+ return this.requestJson(accountPath(accountId, ["company_rules", "apply_all"]), {
174
+ method: "POST"
175
+ });
176
+ }
177
+
118
178
  tags(accountId) {
119
179
  return this.requestJson(accountPath(accountId, ["tags"]));
120
180
  }
@@ -306,6 +366,19 @@ export class AudientiClient {
306
366
  });
307
367
  }
308
368
 
369
+ lockProspect(accountId, prospectId, body = {}) {
370
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "lock"]), {
371
+ method: "POST",
372
+ body
373
+ });
374
+ }
375
+
376
+ unlockProspect(accountId, prospectId) {
377
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "unlock"]), {
378
+ method: "POST"
379
+ });
380
+ }
381
+
309
382
  prospectSequencePreview(accountId, prospectId, body = {}) {
310
383
  return this.requestJson(accountPath(accountId, ["prospects", prospectId, "sequence_preview"]), {
311
384
  method: "POST",
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
 
@@ -28,16 +28,27 @@ const DEFAULT_PROFILE_IDENTIFIERS = [
28
28
  ];
29
29
  const DELETE_CONFIRMATION_VALUES = new Set(["yes", "true", "y"]);
30
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"]);
31
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>]";
32
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>]";
33
34
  const PROSPECTS_ADD_PROFILE_USAGE = "Usage: audienti prospects add-profile <prsp_id> --url <profile_url|email|phone> [--json] [--account <acct_id>]";
34
35
  const PROSPECTS_REPORT_BAD_PROFILE_USAGE = "Usage: audienti prospects report-bad-profile <prsp_id> <prof_id|citation_id> [--json] [--account <acct_id>]";
35
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>]";
36
38
  const PROSPECTS_REJECT_USAGE = "Usage: audienti prospects reject <prsp_id> [--json] [--account <acct_id>]";
37
39
  const PROSPECTS_NURTURE_USAGE = "Usage: audienti prospects nurture <prsp_id> [--reason <nurture|non_responsive|not_fit>] [--json] [--account <acct_id>]";
38
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>]";
39
43
  const PROSPECTS_CHECK_USAGE = "Usage: audienti prospects check [--json|--csv] [filters] [--account <acct_id>]";
40
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>]";
41
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>]";
42
53
  const OFFERS_SHOW_USAGE = "Usage: audienti offers show <offr_id> [--json] [--account <acct_id>]";
43
54
  const OFFERS_UPDATE_USAGE = "Usage: audienti offers update <offr_id> [--name <text>] [--description <text>] [--url <url>] [--json] [--account <acct_id>]";
@@ -157,6 +168,15 @@ async function dispatch(argv, context) {
157
168
  if (normalizedResource === "icps" && action === "add-tag") return icpsTagMutation("add", rest, context, { accountOverride });
158
169
  if (normalizedResource === "icps" && action === "remove-tag") return icpsTagMutation("remove", rest, context, { accountOverride });
159
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 });
160
180
  if (normalizedResource === "tags" && action === "list") return tagsList(rest, context, { accountOverride });
161
181
  if (normalizedResource === "tags" && action === "show") return tagsShow(rest, context, { accountOverride });
162
182
  if (normalizedResource === "lists" && action === "list") return listsList(rest, context, { accountOverride });
@@ -188,9 +208,12 @@ async function dispatch(argv, context) {
188
208
  if (normalizedResource === "prospects" && action === "check") return prospectsCheck(rest, context, { accountOverride });
189
209
  if (normalizedResource === "prospects" && action === "show") return prospectsShow(rest, context, { accountOverride });
190
210
  if (normalizedResource === "prospects" && action === "assign") return prospectsAssign(rest, context, { accountOverride });
211
+ if (normalizedResource === "prospects" && action === "set-status") return prospectsSetStatus(rest, context, { accountOverride });
191
212
  if (normalizedResource === "prospects" && action === "reject") return prospectsDisposition("reject", rest, context, { accountOverride });
192
213
  if (normalizedResource === "prospects" && action === "nurture") return prospectsDisposition("nurture", rest, context, { accountOverride });
193
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 });
194
217
  if (normalizedResource === "prospects" && action === "timeline") return prospectsTimeline(rest, context, { accountOverride });
195
218
  if (normalizedResource === "prospects" && action === "message-types") return prospectsMessageTypes(rest, context, { accountOverride });
196
219
  if (normalizedResource === "prospects" && action === "write") return prospectsWrite(rest, context, { accountOverride });
@@ -263,6 +286,7 @@ function normalizeTopicParts(parts) {
263
286
  function normalizeResource(resource) {
264
287
  if (resource === "principals") return "users";
265
288
  if (resource === "writers") return "writer";
289
+ if (resource === "company_rules" || resource === "company-rules") return "company-rules";
266
290
  return resource === "plays" ? "motions" : resource;
267
291
  }
268
292
 
@@ -813,6 +837,134 @@ async function companiesSearch(args, context, { accountOverride } = {}) {
813
837
  renderCompanies(payload, context);
814
838
  }
815
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
+
816
968
  async function tagsList(args, context, { accountOverride } = {}) {
817
969
  const { values, positionals } = parseCommandArgs(args, jsonOptions());
818
970
  if (positionals.length > 0) throw new CommandError("Usage: audienti tags list [--json] [--account <acct_id>]");
@@ -1489,6 +1641,59 @@ async function prospectsDisposition(action, args, context, { accountOverride } =
1489
1641
  renderProspectDisposition(payload, context, { action });
1490
1642
  }
1491
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
+
1492
1697
  async function prospectsTimeline(args, context, { accountOverride } = {}) {
1493
1698
  const { values, positionals } = parseCommandArgs(args, {
1494
1699
  ...jsonOptions(),
@@ -2599,6 +2804,43 @@ function tagList(value) {
2599
2804
  .filter((tag, index, tags) => tags.indexOf(tag) === index);
2600
2805
  }
2601
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
+
2602
2844
  function filterRecordsByTag(records, value, field) {
2603
2845
  const tag = tagList(value)?.[0];
2604
2846
  if (!tag) return records;
@@ -3117,6 +3359,54 @@ function renderCompanies(payload, context) {
3117
3359
  }
3118
3360
  }
3119
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
+
3120
3410
  async function performBulkMutation(perform) {
3121
3411
  try {
3122
3412
  return { payload: await perform(), rejected: false };
@@ -3265,12 +3555,18 @@ function renderProspectDisposition(payload, context, { action }) {
3265
3555
  const actionLabel = {
3266
3556
  reject: "Rejected",
3267
3557
  nurture: "Moved to nurture",
3268
- restore: "Restored"
3558
+ restore: "Restored",
3559
+ "set-status": "Set status for",
3560
+ lock: "Locked",
3561
+ unlock: "Unlocked"
3269
3562
  }[action] || display(payload?.status, "Updated");
3270
3563
 
3271
3564
  writeLine(context.stdout, `${actionLabel} prospect ${display(prospect.display_name || prospect.name)} (${display(prospect.prefix_id)}).`);
3272
3565
  if (accountProspect.status) writeLine(context.stdout, `Status: ${accountProspect.status}`);
3273
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}`);
3274
3570
  if (payload?.system_list?.name) writeLine(context.stdout, `List: ${payload.system_list.name} (${display(payload.system_list.prefix_id)})`);
3275
3571
  }
3276
3572
 
@@ -4546,9 +4842,12 @@ const HELP_TOPICS = new Map([
4546
4842
  " audienti prospects check [filters]",
4547
4843
  " audienti prospects show <prsp_id>",
4548
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>]",
4549
4847
  " audienti prospects reject <prsp_id>",
4550
4848
  " audienti prospects nurture <prsp_id> [--reason <reason>]",
4551
4849
  " audienti prospects restore <prsp_id>",
4850
+ " audienti prospects unlock <prsp_id>",
4552
4851
  " audienti prospects timeline <prsp_id>",
4553
4852
  " audienti prospects import <linkedin_url> [--motion <motn_id>]",
4554
4853
  " audienti prospects import-batch --file <csv|jsonl|json>",
@@ -4572,6 +4871,10 @@ const HELP_TOPICS = new Map([
4572
4871
  " audienti icps add-tag <icp_id> <tag>",
4573
4872
  " audienti icps remove-tag <icp_id> <tag>",
4574
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>",
4575
4878
  "",
4576
4879
  " Writer",
4577
4880
  " audienti writer test-run <prsp_id>",
@@ -5155,6 +5458,148 @@ const HELP_TOPICS = new Map([
5155
5458
  " companies[].location: string | null"
5156
5459
  ].join("\n")],
5157
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", [
5590
+ "Usage:",
5591
+ ` ${COMPANY_RULES_APPLY_USAGE.slice("Usage: ".length)}`,
5592
+ "",
5593
+ "Status: implemented",
5594
+ "",
5595
+ "Behavior:",
5596
+ " Backfills active visible account prospects through the same company-rule applicator used after profile writes.",
5597
+ "",
5598
+ "API:",
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
+
5158
5603
  ["tags", [
5159
5604
  "Usage:",
5160
5605
  " audienti tags list [--json]",
@@ -5867,9 +6312,12 @@ const HELP_TOPICS = new Map([
5867
6312
  " audienti prospects check [--json|--csv] [filters]",
5868
6313
  " audienti prospects show <prsp_id> [--json]",
5869
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]",
5870
6316
  " audienti prospects reject <prsp_id> [--json]",
5871
6317
  " audienti prospects nurture <prsp_id> [--reason <reason>] [--json]",
5872
6318
  " audienti prospects restore <prsp_id> [--json]",
6319
+ " audienti prospects lock <prsp_id> [--note <text>] [--json]",
6320
+ " audienti prospects unlock <prsp_id> [--json]",
5873
6321
  " audienti prospects timeline <prsp_id> [--json]",
5874
6322
  " audienti prospects message-types <prsp_id> [--json]",
5875
6323
  " audienti prospects write <prsp_id> --type <surface_key> [--json]",
@@ -5883,7 +6331,7 @@ const HELP_TOPICS = new Map([
5883
6331
  " audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--json]",
5884
6332
  " audienti prospects import-status <primp_id> [--json]",
5885
6333
  "",
5886
- "Status: read commands, assignment, disposition, per-prospect draft preview, sequence preview, and import implemented",
6334
+ "Status: read commands, assignment, disposition, lock/unlock, per-prospect draft preview, sequence preview, and import implemented",
5887
6335
  "",
5888
6336
  "Filters:",
5889
6337
  " --query <text>",
@@ -6012,6 +6460,27 @@ const HELP_TOPICS = new Map([
6012
6460
  " }"
6013
6461
  ].join("\n")],
6014
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
+
6015
6484
  ["prospects show", [
6016
6485
  "Usage:",
6017
6486
  " audienti prospects show <prsp_id> [--json] [--account <acct_id>]",
@@ -6067,6 +6536,36 @@ const HELP_TOPICS = new Map([
6067
6536
  " POST /api/v1/accounts/:account_id/prospects/:id/restore.json"
6068
6537
  ].join("\n")],
6069
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
+
6070
6569
  ["prospects timeline", [
6071
6570
  "Usage:",
6072
6571
  " audienti prospects timeline <prsp_id> [--json] [--types <post,comment,reaction>] [--limit <n>] [--account <acct_id>]",
@@ -6776,9 +7275,12 @@ const HELP_TOPICS = new Map([
6776
7275
  " audienti prospects add-profile <prsp_id> --url prospect@example.com",
6777
7276
  " audienti prospects report-bad-profile <prsp_id> <prof_id>",
6778
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\"",
6779
7280
  " audienti prospects reject <prsp_id>",
6780
7281
  " audienti prospects nurture <prsp_id>",
6781
7282
  " audienti prospects restore <prsp_id>",
7283
+ " audienti prospects unlock <prsp_id>",
6782
7284
  " audienti prospects sequence-preview <prsp_id>",
6783
7285
  " audienti writer test-run <prsp_id>",
6784
7286
  " audienti prospects sequence-export <prsp_id> --csv",