@audienti/cli 0.1.33 → 0.1.35

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,29 @@ All notable changes to the Audienti CLI are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.35] - 2026-08-18
8
+
9
+ ### Added
10
+
11
+ - Add `audienti motions abm-companies` commands for listing, adding, and removing motion-scoped positive company filters through the account API.
12
+ - Surface ICP `seniority_match_mode`, motion inbound channels, LOPA profile rows, signal rows, and ABM company filters in CLI/API readbacks.
13
+
14
+ ### Fixed
15
+
16
+ - Reject `posting_language` on non-hiring signal rows instead of accepting a field that cannot persist outside company-scope hiring signals.
17
+
18
+ ## [0.1.34] - 2026-08-17
19
+
20
+ ### Added
21
+
22
+ - Add `audienti icps update <icp_id> --payload <file.json>` for rich ICP facet patches using the same payload shape as ICP creation.
23
+ - Surface executable motion configuration validity in `audienti motions status`, including missing outbound signals and missing LOPA tracked profile URLs.
24
+ - Add outbound motion signal configuration to `audienti motions create --payload` and `audienti motions update <motn_id> --payload <file.json>`, including scoped company/person/both signal rows.
25
+
26
+ ### Changed
27
+
28
+ - Default omitted inbound motion channels to LinkedIn-only when motions are created through the CLI/API contract; Reddit remains explicit opt-in.
29
+
7
30
  ## [0.1.33] - 2026-08-13
8
31
 
9
32
  ### Added
package/README.md CHANGED
@@ -140,8 +140,11 @@ audienti writer test-run <prsp_id>
140
140
  audienti motions analytics <motn_id>
141
141
  audienti motions run-discovery <motn_id>
142
142
  audienti motions quick-start --url https://example.com --wait --confirm
143
+ audienti motions abm-companies <motn_id> add --file abm-domains.txt
144
+ audienti motions abm-companies <motn_id> list
143
145
  audienti motions update <motn_id> --status paused
144
146
  audienti motions update <motn_id> --own-post-engagement true
147
+ audienti motions update <motn_id> --payload motion-signals.json
145
148
  audienti motions activate <motn_id>
146
149
  audienti motions delete <motn_id> --confirm yes
147
150
  audienti content programs
@@ -249,12 +252,36 @@ used by the operator surface:
249
252
  audienti motions run-discovery <motn_id>
250
253
  ```
251
254
 
255
+ To check whether a motion has executable configuration before preparing,
256
+ activating, or running discovery:
257
+
258
+ ```bash
259
+ audienti motions status <motn_id>
260
+ audienti motions status <motn_id> --json
261
+ ```
262
+
263
+ The JSON response includes `executable_configuration.valid` plus a reason when
264
+ the config is incomplete, such as missing outbound signals or missing LOPA
265
+ tracked profile URLs.
266
+
252
267
  To create and launch a quick-start motion from a company URL:
253
268
 
254
269
  ```bash
255
270
  audienti motions quick-start --url https://example.com --wait --confirm
256
271
  ```
257
272
 
273
+ To create or update an ICP from a rich JSON payload:
274
+
275
+ ```bash
276
+ audienti icps create --payload icp.json
277
+ audienti icps update <icp_id> --payload icp-patch.json
278
+ ```
279
+
280
+ `icps update --payload` accepts the same human-readable facet keys as create.
281
+ Supplied facet collections, such as `company_sizes_attributes`, replace that
282
+ collection in place; omitted fields and collections remain unchanged. Invalid
283
+ lookup values fail server-side without partially applying the patch.
284
+
258
285
  To audit one account user's outbound actions, optionally narrowed to one motion
259
286
  and one AccountProspect.created_at cohort:
260
287
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -73,6 +73,8 @@ Useful MCP tools:
73
73
  mapping URLs.
74
74
  - `offers.create`, `icps.create`, and `motions.create` set up the offer, ICP,
75
75
  and play.
76
+ - Use `audienti motions abm-companies <motn_id> add --file <txt|json>` to attach
77
+ a positive company filter list for isolated ABM discovery runs.
76
78
  - `analytics.stages` returns stage conversion cohorts and stage aging.
77
79
  - `analytics.cohort_lists.create` materializes event cohorts as reusable lists.
78
80
 
@@ -101,6 +103,8 @@ audienti users activity me --window 7d --json
101
103
  audienti prospects import-batch --file prospects.csv --motion <motn_id> --assigned-user me --json
102
104
  audienti lists create --name "Target list" --json
103
105
  audienti motions quick-start --url https://example.com --wait --confirm --json
106
+ audienti motions abm-companies <motn_id> add --file abm-domains.txt --json
107
+ audienti motions abm-companies <motn_id> list --json
104
108
  audienti motions update <motn_id> --status paused --json
105
109
  audienti motions activate <motn_id> --json
106
110
  audienti motions delete <motn_id> --confirm yes --json
package/src/api-client.js CHANGED
@@ -290,6 +290,23 @@ export class AudientiClient {
290
290
  });
291
291
  }
292
292
 
293
+ motionAbmCompanies(accountId, motionId) {
294
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "abm_companies"]));
295
+ }
296
+
297
+ addMotionAbmCompanies(accountId, motionId, body) {
298
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "abm_companies"]), {
299
+ method: "POST",
300
+ body
301
+ });
302
+ }
303
+
304
+ removeMotionAbmCompany(accountId, motionId, rowId) {
305
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "abm_companies", rowId]), {
306
+ method: "DELETE"
307
+ });
308
+ }
309
+
293
310
  contentPrograms(accountId, query = {}) {
294
311
  return this.requestJson(accountPath(accountId, ["content_ops", "programs"], query));
295
312
  }
package/src/cli.js CHANGED
@@ -72,7 +72,7 @@ const OFFERS_DELETE_USAGE = "Usage: audienti offers delete <offr_id> --confirm <
72
72
  const WRITER_TEST_RUN_USAGE = "Usage: audienti writer test-run <prsp_id> [--json] [--mode <plan|report|step>] [--branch <both|no-accept|accepted>] [--step <step_key|row_number>] [--report <rprt_id>] [--no-wait] [--timeout-seconds <n>] [--poll-interval-seconds <n>] [--account <acct_id>]";
73
73
  const WRITER_TEST_RUN_SHOW_USAGE = "Usage: audienti writer test-run show <prsp_id> <rprt_id> [--json] [--account <acct_id>]";
74
74
  const MOTIONS_ANALYTICS_USAGE = "Usage: audienti motions analytics <motn_id> [--window 30d] [--json] [--account <acct_id>]";
75
- const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> [--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--own-post-engagement <true|false>] [--json] [--account <acct_id>]";
75
+ const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> ([--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--own-post-engagement <true|false>] | --payload <file.json>) [--json] [--account <acct_id>]";
76
76
  const CONTENT_PROGRAMS_USAGE = "Usage: audienti content programs [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
77
77
  const CONTENT_PLAN_USAGE = "Usage: audienti content plan <cprg_id> [--week <n>] [--due] [--json] [--account <acct_id>]";
78
78
  const CONTENT_SHOW_USAGE = "Usage: audienti content show <cpwi_id> [--json] [--account <acct_id>]";
@@ -90,8 +90,11 @@ const MOTIONS_CLONE_USAGE = "Usage: audienti motions clone <motn_id> --name <tex
90
90
  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>]";
91
91
  const MOTIONS_RUN_DISCOVERY_USAGE = "Usage: audienti motions run-discovery <motn_id> [--target-count <n>] [--json] [--account <acct_id>]";
92
92
  const MOTIONS_QUICK_START_USAGE = "Usage: audienti motions quick-start --url <company_url> [--principal <account_user_id|me>] [--feedback <text>] [--offer-type <type>] [--force] [--confirm] [--wait] [--timeout-seconds <n>] [--poll-interval-seconds <n>] [--json] [--account <acct_id>]";
93
+ const MOTIONS_ABM_COMPANIES_LIST_USAGE = "Usage: audienti motions abm-companies <motn_id> list [--json] [--account <acct_id>]";
94
+ const MOTIONS_ABM_COMPANIES_ADD_USAGE = "Usage: audienti motions abm-companies <motn_id> add (<domain_or_linkedin_url>... | --file <txt|json>) [--json] [--account <acct_id>]";
95
+ const MOTIONS_ABM_COMPANIES_REMOVE_USAGE = "Usage: audienti motions abm-companies <motn_id> remove <row_id> [--json] [--account <acct_id>]";
93
96
  const ICPS_SHOW_USAGE = "Usage: audienti icps show <icp_id> [--json] [--account <acct_id>]";
94
- const ICPS_UPDATE_USAGE = "Usage: audienti icps update <icp_id> [--name <text>] [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]";
97
+ const ICPS_UPDATE_USAGE = "Usage: audienti icps update <icp_id> ([--name <text>] [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] | --payload <file.json>) [--json] [--account <acct_id>]";
95
98
  const ICPS_ADD_TAG_USAGE = "Usage: audienti icps add-tag <icp_id> <tag> [--json] [--account <acct_id>]";
96
99
  const ICPS_REMOVE_TAG_USAGE = "Usage: audienti icps remove-tag <icp_id> <tag> [--json] [--account <acct_id>]";
97
100
  const LISTS_ADD_TAG_USAGE = "Usage: audienti lists add-tag <list_id> <tag> [--json] [--account <acct_id>]";
@@ -237,6 +240,7 @@ async function dispatch(argv, context) {
237
240
  if (normalizedResource === "motions" && action === "list") return motionsList(rest, context, { accountOverride });
238
241
  if (normalizedResource === "motions" && action === "show") return motionsShow(rest, context, { accountOverride });
239
242
  if (normalizedResource === "motions" && action === "status") return motionsStatus(rest, context, { accountOverride });
243
+ if (normalizedResource === "motions" && ["abm-companies", "company-filters"].includes(action)) return motionsAbmCompanies(rest, context, { accountOverride });
240
244
  if (normalizedResource === "motions" && action === "analytics") return motionsAnalytics(rest, context, { accountOverride });
241
245
  if (normalizedResource === "motions" && action === "prospects") return motionsProspects(rest, context, { accountOverride });
242
246
  if (normalizedResource === "motions" && action === "add-prospects") return motionsAddProspects(rest, context, { accountOverride });
@@ -1030,25 +1034,19 @@ async function icpsCreate(args, context, { accountOverride } = {}) {
1030
1034
  async function icpsUpdate(args, context, { accountOverride } = {}) {
1031
1035
  const { values, positionals } = parseCommandArgs(args, {
1032
1036
  ...jsonOptions(),
1037
+ payload: { type: "string" },
1033
1038
  name: { type: "string" },
1034
1039
  notes: { type: "string" },
1035
1040
  tags: { type: "string" },
1036
1041
  "discovery-keyword": { type: "string" }
1037
1042
  });
1038
- const hasUpdateField = values.name || values.notes !== undefined || values.tags !== undefined || values["discovery-keyword"] !== undefined;
1043
+ const hasUpdateField = values.payload || icpSimpleFieldsPresent(values);
1039
1044
  if (positionals.length !== 1 || !hasUpdateField) {
1040
1045
  throw new CommandError(ICPS_UPDATE_USAGE);
1041
1046
  }
1042
1047
 
1043
1048
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
1044
- const icp = await client.updateIcp(accountId, positionals[0], {
1045
- icp: compactObject({
1046
- name: values.name,
1047
- notes: values.notes,
1048
- discovery_keyword: values["discovery-keyword"],
1049
- tags: values.tags !== undefined ? tagList(values.tags) : undefined
1050
- })
1051
- });
1049
+ const icp = await client.updateIcp(accountId, positionals[0], { icp: await icpUpdatePayload(values) });
1052
1050
  if (values.json) return writeJson(context.stdout, icp);
1053
1051
 
1054
1052
  writeLine(context.stdout, `Updated ICP ${display(icp?.name)} (${display(icp?.prefix_id)}).`);
@@ -1683,7 +1681,7 @@ async function motionsCreate(args, context, { accountOverride } = {}) {
1683
1681
  }
1684
1682
 
1685
1683
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
1686
- const payload = await readJsonPayload(values.payload);
1684
+ const payload = normalizeMotionCreatePayload(await readJsonPayload(values.payload));
1687
1685
  const created = await client.createMotion(accountId, { motion: payload });
1688
1686
  if (values.json) return writeJson(context.stdout, created);
1689
1687
 
@@ -1691,6 +1689,70 @@ async function motionsCreate(args, context, { accountOverride } = {}) {
1691
1689
  renderMotion(created, context);
1692
1690
  }
1693
1691
 
1692
+ function normalizeMotionCreatePayload(payload) {
1693
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload;
1694
+ validateMotionSignalRows(payload);
1695
+ if (String(payload.kind || "").trim().toLowerCase() !== "inbound") return payload;
1696
+ if (Object.prototype.hasOwnProperty.call(payload, "inbound_channels")) return payload;
1697
+
1698
+ return { ...payload, inbound_channels: ["linkedin"] };
1699
+ }
1700
+
1701
+ async function motionsAbmCompanies(args, context, { accountOverride } = {}) {
1702
+ const [motionId, subaction, ...rest] = args;
1703
+ if (!motionId || !subaction) throw new CommandError("Usage: audienti motions abm-companies <motn_id> <list|add|remove> [args] [--json] [--account <acct_id>]");
1704
+
1705
+ if (subaction === "list") return motionsAbmCompaniesList(motionId, rest, context, { accountOverride });
1706
+ if (subaction === "add") return motionsAbmCompaniesAdd(motionId, rest, context, { accountOverride });
1707
+ if (["remove", "delete"].includes(subaction)) return motionsAbmCompaniesRemove(motionId, rest, context, { accountOverride });
1708
+
1709
+ throw new CommandError("Usage: audienti motions abm-companies <motn_id> <list|add|remove> [args] [--json] [--account <acct_id>]");
1710
+ }
1711
+
1712
+ async function motionsAbmCompaniesList(motionId, args, context, { accountOverride } = {}) {
1713
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1714
+ if (positionals.length > 0) throw new CommandError(MOTIONS_ABM_COMPANIES_LIST_USAGE);
1715
+
1716
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1717
+ const payload = await client.motionAbmCompanies(accountId, motionId);
1718
+ if (values.json) return writeJson(context.stdout, payload);
1719
+
1720
+ renderMotionAbmCompanies(payload, context);
1721
+ }
1722
+
1723
+ async function motionsAbmCompaniesAdd(motionId, args, context, { accountOverride } = {}) {
1724
+ const { values, positionals } = parseCommandArgs(args, {
1725
+ ...jsonOptions(),
1726
+ file: { type: "string" }
1727
+ });
1728
+ if ((positionals.length === 0 && !values.file) || (positionals.length > 0 && values.file)) {
1729
+ throw new CommandError(MOTIONS_ABM_COMPANIES_ADD_USAGE);
1730
+ }
1731
+
1732
+ const entries = values.file ? await motionAbmCompanyEntriesFromFile(values.file) : positionals;
1733
+ if (entries.length === 0) throw new CommandError(MOTIONS_ABM_COMPANIES_ADD_USAGE);
1734
+
1735
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1736
+ const payload = await client.addMotionAbmCompanies(accountId, motionId, { abm_companies: entries });
1737
+ if (values.json) return writeJson(context.stdout, payload);
1738
+
1739
+ writeLine(context.stdout, `Company filters: ${Array.isArray(payload?.abm_companies) ? payload.abm_companies.length : 0}`);
1740
+ renderMotionAbmCompanies(payload, context);
1741
+ renderMotionAbmCompanyErrors(payload, context);
1742
+ }
1743
+
1744
+ async function motionsAbmCompaniesRemove(motionId, args, context, { accountOverride } = {}) {
1745
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1746
+ if (positionals.length !== 1) throw new CommandError(MOTIONS_ABM_COMPANIES_REMOVE_USAGE);
1747
+
1748
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1749
+ const payload = await client.removeMotionAbmCompany(accountId, motionId, positionals[0]);
1750
+ if (values.json) return writeJson(context.stdout, payload);
1751
+
1752
+ writeLine(context.stdout, `Removed company filter ${display(positionals[0])} from motion ${display(payload?.motion_id || motionId)}.`);
1753
+ renderMotionAbmCompanies(payload, context);
1754
+ }
1755
+
1694
1756
  async function motionsDelete(args, context, { accountOverride } = {}) {
1695
1757
  const { values, positionals } = parseCommandArgs(args, {
1696
1758
  ...jsonOptions(),
@@ -1711,30 +1773,81 @@ async function motionsDelete(args, context, { accountOverride } = {}) {
1711
1773
  async function motionsUpdate(args, context, { accountOverride } = {}) {
1712
1774
  const { values, positionals } = parseCommandArgs(args, {
1713
1775
  ...jsonOptions(),
1776
+ payload: { type: "string" },
1714
1777
  status: { type: "string" },
1715
1778
  tags: { type: "string" },
1716
1779
  "own-post-engagement": { type: "string" }
1717
1780
  });
1718
- const hasUpdateField = values.status || values.tags !== undefined || values["own-post-engagement"] !== undefined;
1781
+ const hasUpdateField = values.payload || motionSimpleFieldsPresent(values);
1719
1782
  if (positionals.length !== 1 || !hasUpdateField) {
1720
1783
  throw new CommandError(MOTIONS_UPDATE_USAGE);
1721
1784
  }
1722
1785
 
1723
- const normalizedStatus = normalizeMotionStatus(values.status);
1724
- const motionAttributes = compactObject({
1725
- status: normalizedStatus,
1726
- play_tags: values.tags !== undefined ? tagList(values.tags) : undefined,
1727
- own_post_engagement: values["own-post-engagement"] !== undefined ? parseBooleanString(values["own-post-engagement"], "--own-post-engagement") : undefined
1728
- });
1729
-
1730
1786
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
1731
- const motion = await client.updateMotion(accountId, positionals[0], { motion: motionAttributes });
1787
+ const motion = await client.updateMotion(accountId, positionals[0], { motion: await motionUpdatePayload(values) });
1732
1788
  if (values.json) return writeJson(context.stdout, motion);
1733
1789
 
1734
1790
  writeLine(context.stdout, `Updated motion ${display(motion?.name)} (${display(motion?.prefix_id)}).`);
1735
1791
  renderMotion(motion, context);
1736
1792
  }
1737
1793
 
1794
+ async function motionUpdatePayload(values) {
1795
+ if (values.payload) {
1796
+ if (motionSimpleFieldsPresent(values)) {
1797
+ throw new CommandError("Choose one motion input mode: either --payload <file.json> or the simple --status/--tags/--own-post-engagement flags.");
1798
+ }
1799
+
1800
+ const payload = await readJsonPayload(values.payload);
1801
+ validateMotionSignalRows(payload);
1802
+ return payload;
1803
+ }
1804
+
1805
+ return compactObject({
1806
+ status: normalizeMotionStatus(values.status),
1807
+ play_tags: values.tags !== undefined ? tagList(values.tags) : undefined,
1808
+ own_post_engagement: values["own-post-engagement"] !== undefined ? parseBooleanString(values["own-post-engagement"], "--own-post-engagement") : undefined
1809
+ });
1810
+ }
1811
+
1812
+ function motionSimpleFieldsPresent(values) {
1813
+ return Boolean(values.status) || values.tags !== undefined || values["own-post-engagement"] !== undefined;
1814
+ }
1815
+
1816
+ function validateMotionSignalRows(payload) {
1817
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
1818
+
1819
+ for (const row of Array.isArray(payload.signal_rows) ? payload.signal_rows : []) {
1820
+ if (!row || typeof row !== "object" || Array.isArray(row)) continue;
1821
+ if (!String(row.posting_language || "").trim()) continue;
1822
+
1823
+ const scope = String(row.scope || "company").trim().toLowerCase();
1824
+ const category = String(row.company_signal_category || "").trim().toLowerCase();
1825
+ if (scope === "company" && category === "hiring") continue;
1826
+
1827
+ throw new CommandError("posting_language is only supported on company-scope hiring signal rows.");
1828
+ }
1829
+ }
1830
+
1831
+ async function motionAbmCompanyEntriesFromFile(path) {
1832
+ const contents = await readFile(path, "utf8");
1833
+ const trimmed = contents.trim();
1834
+ if (!trimmed) return [];
1835
+
1836
+ try {
1837
+ const parsed = JSON.parse(trimmed);
1838
+ if (Array.isArray(parsed)) return parsed;
1839
+ if (parsed && typeof parsed === "object") {
1840
+ return Array.isArray(parsed.abm_companies) ? parsed.abm_companies :
1841
+ (Array.isArray(parsed.company_filters) ? parsed.company_filters :
1842
+ (Array.isArray(parsed.items) ? parsed.items : []));
1843
+ }
1844
+ } catch {
1845
+ // Plain text files use one company domain or LinkedIn company URL per line.
1846
+ }
1847
+
1848
+ return trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1849
+ }
1850
+
1738
1851
  async function motionsStatusShortcut(action, args, context, { accountOverride } = {}) {
1739
1852
  const usage = `Usage: audienti motions ${action} <motn_id> [--json] [--account <acct_id>]`;
1740
1853
  const { values, positionals } = parseCommandArgs(args, jsonOptions());
@@ -3575,7 +3688,7 @@ function filterRecordsByTag(records, value, field) {
3575
3688
 
3576
3689
  async function icpCreatePayload(values) {
3577
3690
  if (values.payload) {
3578
- if (values.name || values.notes || values.tags !== undefined || values["discovery-keyword"]) {
3691
+ if (icpSimpleFieldsPresent(values)) {
3579
3692
  throw new CommandError("Choose one ICP input mode: either --payload <file.json> or the simple --name/--notes/--discovery-keyword/tags flags.");
3580
3693
  }
3581
3694
 
@@ -3594,6 +3707,27 @@ async function icpCreatePayload(values) {
3594
3707
  });
3595
3708
  }
3596
3709
 
3710
+ async function icpUpdatePayload(values) {
3711
+ if (values.payload) {
3712
+ if (icpSimpleFieldsPresent(values)) {
3713
+ throw new CommandError("Choose one ICP input mode: either --payload <file.json> or the simple --name/--notes/--discovery-keyword/tags flags.");
3714
+ }
3715
+
3716
+ return readJsonPayload(values.payload);
3717
+ }
3718
+
3719
+ return compactObject({
3720
+ name: values.name,
3721
+ notes: values.notes,
3722
+ tags: values.tags !== undefined ? tagList(values.tags) : undefined,
3723
+ discovery_keyword: values["discovery-keyword"]
3724
+ });
3725
+ }
3726
+
3727
+ function icpSimpleFieldsPresent(values) {
3728
+ return Boolean(values.name) || values.notes !== undefined || values.tags !== undefined || values["discovery-keyword"] !== undefined;
3729
+ }
3730
+
3597
3731
  async function prospectNotePayload(values, { forcedType, usageText } = {}) {
3598
3732
  const engagementKey = values["engagement-type"] || values["engagement-key"];
3599
3733
 
@@ -4191,7 +4325,7 @@ function renderOffer(offer, context) {
4191
4325
  function renderIcps(icps, context) {
4192
4326
  if (!Array.isArray(icps) || icps.length === 0) return writeLine(context.stdout, "No ICPs found.");
4193
4327
 
4194
- writeLine(context.stdout, "ICP ID\tNAME\tTAGS\tDISCOVERY KEYWORD\tAGENT");
4328
+ writeLine(context.stdout, "ICP ID\tNAME\tTAGS\tDISCOVERY KEYWORD\tSENIORITY MODE\tAGENT");
4195
4329
  for (const icp of icps) {
4196
4330
  writeLine(
4197
4331
  context.stdout,
@@ -4200,6 +4334,7 @@ function renderIcps(icps, context) {
4200
4334
  display(icp.name),
4201
4335
  display(Array.isArray(icp.tags) && icp.tags.length > 0 ? icp.tags.join(",") : "-"),
4202
4336
  display(icp.discovery_keyword),
4337
+ display(icp.seniority_match_mode),
4203
4338
  display(icp.agent?.name)
4204
4339
  ].join("\t")
4205
4340
  );
@@ -4211,6 +4346,7 @@ function renderIcp(icp, context) {
4211
4346
  if (Array.isArray(icp?.tags)) writeLine(context.stdout, `Tags: ${display(icp.tags.join(", "), "-")}`);
4212
4347
  if (icp?.notes) writeLine(context.stdout, `Notes: ${icp.notes}`);
4213
4348
  if (icp?.discovery_keyword) writeLine(context.stdout, `Discovery keyword: ${icp.discovery_keyword}`);
4349
+ if (icp?.seniority_match_mode) writeLine(context.stdout, `Seniority match mode: ${icp.seniority_match_mode}`);
4214
4350
  if (icp?.agent?.name) writeLine(context.stdout, `Agent: ${icp.agent.name}`);
4215
4351
  }
4216
4352
 
@@ -4323,10 +4459,15 @@ function renderMotion(motion, context) {
4323
4459
  writeLine(context.stdout, `Motion: ${display(motion?.name)} (${display(motion?.prefix_id)})`);
4324
4460
  writeLine(context.stdout, `Status: ${display(motion?.status)}`);
4325
4461
  writeLine(context.stdout, `Kind: ${display(motion?.kind)}`);
4462
+ renderExecutableConfiguration(motion?.executable_configuration, context);
4326
4463
  if (motion?.offer?.name) writeLine(context.stdout, `Offer: ${motion.offer.name} (${display(motion.offer.prefix_id)})`);
4327
4464
  if (motion?.icp?.name) writeLine(context.stdout, `ICP: ${motion.icp.name} (${display(motion.icp.prefix_id)})`);
4328
4465
  if (motion?.list?.name) writeLine(context.stdout, `List: ${motion.list.name} (${display(motion.list.prefix_id)})`);
4329
4466
  writeLine(context.stdout, `Own-post engagement: ${motion?.own_post_engagement ? "enabled" : "disabled"}`);
4467
+ if (Array.isArray(motion?.inbound_channels)) writeLine(context.stdout, `Inbound channels: ${display(motion.inbound_channels.join(", "), "-")}`);
4468
+ renderMotionLopaProfiles(motion?.lopa_profiles, context);
4469
+ renderMotionSignalRows(motion?.signal_rows, context);
4470
+ renderMotionAbmCompanies(motion, context, { showEmpty: false });
4330
4471
  if (Array.isArray(motion?.play_tags)) writeLine(context.stdout, `Tags: ${display(motion.play_tags.join(", "), "-")}`);
4331
4472
  if (motion?.principal_account_user?.id) {
4332
4473
  writeLine(
@@ -4336,6 +4477,55 @@ function renderMotion(motion, context) {
4336
4477
  }
4337
4478
  }
4338
4479
 
4480
+ function renderMotionLopaProfiles(rows, context) {
4481
+ if (!Array.isArray(rows) || rows.length === 0) return;
4482
+
4483
+ writeLine(context.stdout, "LOPA profiles:");
4484
+ for (const row of rows) {
4485
+ writeLine(context.stdout, `- ${display(row.url)} (${display(row.source_type, "other")})`);
4486
+ }
4487
+ }
4488
+
4489
+ function renderMotionSignalRows(rows, context) {
4490
+ if (!Array.isArray(rows) || rows.length === 0) return;
4491
+
4492
+ writeLine(context.stdout, "Signal rows:");
4493
+ for (const row of rows) {
4494
+ const filters = [
4495
+ row.company_signal_category ? `category=${row.company_signal_category}` : null,
4496
+ row.role_terms ? `roles=${singleLine(row.role_terms)}` : null,
4497
+ row.posting_language ? `posting_language=${singleLine(row.posting_language)}` : null,
4498
+ row.topics ? `topics=${singleLine(row.topics)}` : null
4499
+ ].filter(Boolean).join("; ");
4500
+ writeLine(context.stdout, `- ${display(row.scope)}: ${display(row.question)}${filters ? ` [${filters}]` : ""}`);
4501
+ }
4502
+ }
4503
+
4504
+ function renderMotionAbmCompanies(payload, context, { showEmpty = true } = {}) {
4505
+ const rows = Array.isArray(payload?.abm_companies) ? payload.abm_companies : [];
4506
+ if (rows.length === 0) {
4507
+ if (showEmpty) writeLine(context.stdout, "No company filters found.");
4508
+ return;
4509
+ }
4510
+
4511
+ writeAlignedTable(context, ["ROW ID", "KIND", "VALUE", "STATUS"], rows.map((row) => [
4512
+ display(row.id),
4513
+ display(row.kind),
4514
+ display(row.normalized_value || row.raw_value),
4515
+ display(row.status)
4516
+ ]));
4517
+ }
4518
+
4519
+ function renderMotionAbmCompanyErrors(payload, context) {
4520
+ const errors = Array.isArray(payload?.errors) ? payload.errors : [];
4521
+ if (errors.length === 0) return;
4522
+
4523
+ writeLine(context.stdout, "Errors:");
4524
+ for (const error of errors) {
4525
+ writeLine(context.stdout, `- ${display(error.input)}: ${display(error.error)}`);
4526
+ }
4527
+ }
4528
+
4339
4529
  function renderContentPrograms(programs, context) {
4340
4530
  if (!Array.isArray(programs) || programs.length === 0) return writeLine(context.stdout, "No ContentOps programs found.");
4341
4531
 
@@ -4393,10 +4583,23 @@ function renderMotionStatus(status, context) {
4393
4583
  writeLine(context.stdout, `Motion: ${display(status?.name)} (${display(status?.prefix_id)})`);
4394
4584
  writeLine(context.stdout, `State: ${display(status?.state)}`);
4395
4585
  writeLine(context.stdout, `Reason: ${display(status?.reason_label || status?.reason_key)}`);
4586
+ renderExecutableConfiguration(status?.executable_configuration, context);
4396
4587
  if (status?.description) writeLine(context.stdout, status.description);
4397
4588
  if (status?.action?.label) writeLine(context.stdout, `Action: ${status.action.label}`);
4398
4589
  }
4399
4590
 
4591
+ function renderExecutableConfiguration(config, context) {
4592
+ if (!config || typeof config !== "object") return;
4593
+
4594
+ writeLine(context.stdout, `Valid config: ${config.valid ? "yes" : "no"}`);
4595
+ if (!config.valid && (config.reason_label || config.reason_key)) {
4596
+ writeLine(context.stdout, `Config reason: ${display(config.reason_label || config.reason_key)}`);
4597
+ }
4598
+ if (!config.valid && config.description) {
4599
+ writeLine(context.stdout, `Config detail: ${config.description}`);
4600
+ }
4601
+ }
4602
+
4400
4603
  function renderProspects(payload, context, { wide = false, profiles = false } = {}) {
4401
4604
  const prospects = Array.isArray(payload?.prospects) ? payload.prospects : [];
4402
4605
  if (prospects.length === 0) return writeLine(context.stdout, "No prospects found.");
@@ -5941,6 +6144,10 @@ function display(value, fallback = "") {
5941
6144
  return value === undefined || value === null || value === "" ? fallback : value;
5942
6145
  }
5943
6146
 
6147
+ function singleLine(value) {
6148
+ return String(value || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(", ");
6149
+ }
6150
+
5944
6151
  function prospectsToCsv(prospects) {
5945
6152
  const profileIdentifiers = profileIdentifiersForPayload({ meta: {} }, prospects);
5946
6153
  const headers = [
@@ -6237,8 +6444,11 @@ const HELP_TOPICS = new Map([
6237
6444
  " audienti motions run-discovery <motn_id>",
6238
6445
  " audienti motions quick-start --url <company_url> --confirm --wait",
6239
6446
  " audienti motions prospects <motn_id>",
6447
+ " audienti motions abm-companies <motn_id> list",
6448
+ " audienti motions abm-companies <motn_id> add <domain_or_linkedin_url>...",
6240
6449
  " audienti motions create --payload <file.json>",
6241
6450
  " audienti motions update <motn_id> [--status <state>] [--tags <tag[,tag...]>] [--own-post-engagement <true|false>]",
6451
+ " audienti motions update <motn_id> --payload <file.json>",
6242
6452
  " audienti motions add-tag <motn_id> <tag>",
6243
6453
  " audienti motions remove-tag <motn_id> <tag>",
6244
6454
  " audienti motions activate <motn_id>",
@@ -6293,7 +6503,7 @@ const HELP_TOPICS = new Map([
6293
6503
  " audienti offers delete <offr_id> --confirm <yes|true|Y|y>",
6294
6504
  " audienti icps list [--tag <tag>]",
6295
6505
  " audienti icps show <icp_id>",
6296
- " audienti icps update <icp_id> [--tags <tag[,tag...]>]",
6506
+ " audienti icps update <icp_id> [--tags <tag[,tag...]> | --payload <file.json>]",
6297
6507
  " audienti icps add-tag <icp_id> <tag>",
6298
6508
  " audienti icps remove-tag <icp_id> <tag>",
6299
6509
  " audienti companies search --query <text>",
@@ -6770,7 +6980,7 @@ const HELP_TOPICS = new Map([
6770
6980
  " audienti icps list [--tag <tag>] [--json]",
6771
6981
  " audienti icps show <icp_id> [--json]",
6772
6982
  " audienti icps create (--name <text> | --payload <file.json>) [--json]",
6773
- " audienti icps update <icp_id> [--tags <tag[,tag...]>] [--json]",
6983
+ " audienti icps update <icp_id> ([--tags <tag[,tag...]>] | --payload <file.json>) [--json]",
6774
6984
  " audienti icps add-tag <icp_id> <tag> [--json]",
6775
6985
  " audienti icps remove-tag <icp_id> <tag> [--json]",
6776
6986
  "",
@@ -6863,12 +7073,12 @@ const HELP_TOPICS = new Map([
6863
7073
 
6864
7074
  ["icps update", [
6865
7075
  "Usage:",
6866
- " audienti icps update <icp_id> [--name <text>] [--notes <text>] [--discovery-keyword <text>] [--tags <tag[,tag...]>] [--json] [--account <acct_id>]",
7076
+ ` ${ICPS_UPDATE_USAGE.slice("Usage: ".length)}`,
6867
7077
  "",
6868
7078
  "Status: implemented",
6869
7079
  "",
6870
7080
  "Purpose:",
6871
- " Update simple ICP fields or replace the ICP's full tag set.",
7081
+ " Update simple ICP fields, replace the ICP's full tag set, or apply a rich ICP payload patch.",
6872
7082
  "",
6873
7083
  "Input shape:",
6874
7084
  " icp_id: icpp_ prefixed id or integer id",
@@ -6876,15 +7086,30 @@ const HELP_TOPICS = new Map([
6876
7086
  " notes: string | optional",
6877
7087
  " discovery_keyword: string | optional",
6878
7088
  " tags: comma-separated tag list | optional",
7089
+ " payload: file.json | optional full or partial ICP object using the account API create shape",
7090
+ "",
7091
+ "Behavior:",
7092
+ " Choose either --payload or simple flags. Supplied facet collections replace that collection in place; omitted fields and facet collections remain unchanged.",
7093
+ " Human-readable lookup-backed facet names are resolved by the account API. Invalid lookup values fail with 422 and no partial mutation.",
6879
7094
  "",
6880
7095
  "API:",
6881
7096
  " PATCH /api/v1/accounts/:account_id/icps/:id.json",
6882
7097
  "",
6883
- "JSON body:",
7098
+ "Simple JSON body:",
6884
7099
  " {",
6885
7100
  " \"icp\": {",
6886
7101
  " \"tags\": [\"enterprise\", \"renewal\"]",
6887
7102
  " }",
7103
+ " }",
7104
+ "",
7105
+ "Payload file example:",
7106
+ " {",
7107
+ " \"company_sizes_attributes\": [",
7108
+ " {\"name\": \"1001-5000 employees\"}",
7109
+ " ],",
7110
+ " \"seniorities_attributes\": [",
7111
+ " {\"name\": \"CXO\"}",
7112
+ " ]",
6888
7113
  " }"
6889
7114
  ].join("\n")],
6890
7115
 
@@ -7488,8 +7713,11 @@ const HELP_TOPICS = new Map([
7488
7713
  " audienti motions analytics <motn_id> [--window 30d] [--json]",
7489
7714
  " audienti motions prospects <motn_id> [--json]",
7490
7715
  " audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
7716
+ " audienti motions abm-companies <motn_id> list [--json]",
7717
+ " audienti motions abm-companies <motn_id> add (<domain_or_linkedin_url>... | --file <txt|json>) [--json]",
7718
+ " audienti motions abm-companies <motn_id> remove <row_id> [--json]",
7491
7719
  " audienti motions create --payload <file.json> [--json]",
7492
- " audienti motions update <motn_id> [--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--own-post-engagement <true|false>] [--json]",
7720
+ " audienti motions update <motn_id> ([--status <draft|preparing|active|paused|archived>] [--tags <tag[,tag...]>] [--own-post-engagement <true|false>] | --payload <file.json>) [--json]",
7493
7721
  " audienti motions add-tag <motn_id> <tag> [--json]",
7494
7722
  " audienti motions remove-tag <motn_id> <tag> [--json]",
7495
7723
  " audienti motions activate <motn_id> [--json]",
@@ -7570,10 +7798,38 @@ const HELP_TOPICS = new Map([
7570
7798
  "Input shape:",
7571
7799
  " motn_id: motn_ prefix id",
7572
7800
  "",
7801
+ "Output shape:",
7802
+ " inbound_channels: enabled inbound collectors for inbound motions",
7803
+ " lopa_profiles[]: tracked LinkedIn profile rows for LOPA motions",
7804
+ " signal_rows[]: outbound signal configuration rows",
7805
+ " abm_companies[]: motion-scoped positive company filter rows",
7806
+ "",
7573
7807
  "API:",
7574
7808
  " GET /api/v1/accounts/:account_id/motions/:id.json"
7575
7809
  ].join("\n")],
7576
7810
 
7811
+ ["motions abm-companies", [
7812
+ "Usage:",
7813
+ ` ${MOTIONS_ABM_COMPANIES_LIST_USAGE.slice("Usage: ".length)}`,
7814
+ ` ${MOTIONS_ABM_COMPANIES_ADD_USAGE.slice("Usage: ".length)}`,
7815
+ ` ${MOTIONS_ABM_COMPANIES_REMOVE_USAGE.slice("Usage: ".length)}`,
7816
+ "",
7817
+ "Status: implemented",
7818
+ "",
7819
+ "Purpose:",
7820
+ " Manage a motion-scoped positive company filter list for isolated ABM discovery runs.",
7821
+ "",
7822
+ "Input shape:",
7823
+ " motn_id: motn_ prefix id",
7824
+ " domain_or_linkedin_url: company domain or LinkedIn company page URL",
7825
+ " file: plain text one entry per line, or JSON array / { abm_companies: [...] }",
7826
+ "",
7827
+ "API:",
7828
+ " GET /api/v1/accounts/:account_id/motions/:motion_id/abm_companies.json",
7829
+ " POST /api/v1/accounts/:account_id/motions/:motion_id/abm_companies.json",
7830
+ " DELETE /api/v1/accounts/:account_id/motions/:motion_id/abm_companies/:row_id.json"
7831
+ ].join("\n")],
7832
+
7577
7833
  ["motions status", [
7578
7834
  "Usage:",
7579
7835
  " audienti motions status <motn_id> [--json] [--account <acct_id>]",
@@ -7587,6 +7843,7 @@ const HELP_TOPICS = new Map([
7587
7843
  " state: healthy_idle | broken",
7588
7844
  " reason_key: string",
7589
7845
  " reason_label: string",
7846
+ " executable_configuration: { valid: boolean, reason_key: string | null, reason_label: string | null, description: string | null }",
7590
7847
  " description: string",
7591
7848
  " action: { key: string, label: string } | null",
7592
7849
  " stats: { target_count, deficit, projected_connectable, capacity, daily_target }",
@@ -7738,7 +7995,9 @@ const HELP_TOPICS = new Map([
7738
7995
  " icp_id: icpp_ prefix id | optional",
7739
7996
  " list_id: list_ prefix id | optional",
7740
7997
  " play_tags: [string] | optional",
7741
- " inbound_channels: [linkedin | reddit] | optional",
7998
+ " signal_rows: [{ scope: company | person | both, question: string, company_signal_category: string | optional, role_terms: string | optional, posting_language: hiring-only string | optional, topics: string | optional }] | optional for outbound motions",
7999
+ " signal_questions: newline-delimited company::, person::, or both:: legacy question text | optional for outbound motions",
8000
+ " inbound_channels: [linkedin | reddit] | optional; defaults to [linkedin] for inbound motions",
7742
8001
  " lopa_profiles: [{ url: string, source_type: creator | competitor | partner | customer | other }] | optional",
7743
8002
  "",
7744
8003
  "JSON example:",
@@ -7755,6 +8014,7 @@ const HELP_TOPICS = new Map([
7755
8014
  "",
7756
8015
  "Behavior:",
7757
8016
  " The API calls Motions::Setup and the managed graph provisioner. If principal_account_user_id is omitted, the authenticated account user is used.",
8017
+ " posting_language is only supported on company-scope hiring signal rows; use topics for person discussion evidence.",
7758
8018
  " Use `audienti offers list`, `audienti icps list`, and `audienti users list` to resolve valid ids before calling this command."
7759
8019
  ].join("\n")],
7760
8020
 
@@ -7793,25 +8053,45 @@ const HELP_TOPICS = new Map([
7793
8053
  "Status: implemented",
7794
8054
  "",
7795
8055
  "Purpose:",
7796
- " Change one motion or play's lifecycle status or replace its tag set.",
8056
+ " Change one motion or play's lifecycle status, replace its tag set, or patch rich motion configuration.",
7797
8057
  "",
7798
8058
  "Input shape:",
7799
8059
  " motn_id: motn_ prefix id",
7800
8060
  " status: draft | preparing | active | paused | archived | optional",
7801
8061
  " tags: comma-separated tag list | optional",
8062
+ " payload: file.json | optional full or partial motion object using the account API shape",
7802
8063
  "",
7803
8064
  "Behavior:",
7804
- " Updates only the provided fields. Sending --tags replaces the motion's full tag set.",
8065
+ " Choose either --payload or simple flags. Updates only the provided fields. Sending --tags replaces the motion's full tag set.",
8066
+ " Payload mode supports outbound signal_rows or legacy signal_questions; supplied rows replace the motion's active ready signals.",
8067
+ " posting_language is only supported on company-scope hiring signal rows; use topics for person discussion evidence.",
7805
8068
  "",
7806
8069
  "API:",
7807
8070
  " PATCH /api/v1/accounts/:account_id/motions/:id.json",
7808
8071
  "",
7809
- "JSON body:",
8072
+ "Simple JSON body:",
7810
8073
  " {",
7811
8074
  " \"motion\": {",
7812
8075
  " \"status\": \"paused\",",
7813
8076
  " \"play_tags\": [\"sarit\", \"pj\"]",
7814
8077
  " }",
8078
+ " }",
8079
+ "",
8080
+ "Payload file example:",
8081
+ " {",
8082
+ " \"signal_rows\": [",
8083
+ " {",
8084
+ " \"scope\": \"company\",",
8085
+ " \"question\": \"Is the company hiring revenue operations leaders?\",",
8086
+ " \"company_signal_category\": \"hiring\",",
8087
+ " \"role_terms\": \"Revenue Operations\"",
8088
+ " },",
8089
+ " {",
8090
+ " \"scope\": \"person\",",
8091
+ " \"question\": \"Is the person talking about pipeline quality?\",",
8092
+ " \"topics\": \"pipeline quality\"",
8093
+ " }",
8094
+ " ]",
7815
8095
  " }"
7816
8096
  ].join("\n")],
7817
8097
 
@@ -9209,6 +9489,7 @@ const HELP_TOPICS = new Map([
9209
9489
  "2. Create a motion or play",
9210
9490
  " audienti setup play preflight --principal <account_user_id|me> --platform linkedin",
9211
9491
  " audienti motions create --payload <file.json>",
9492
+ " audienti motions abm-companies <motn_id> add --file abm-domains.txt",
9212
9493
  " audienti motions clone <motn_id> --name \"New subset motion\"",
9213
9494
  " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]",
9214
9495
  " audienti motions activate <motn_id>",