@audienti/cli 0.1.35 → 0.1.37

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,22 @@ All notable changes to the Audienti CLI are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.37] - 2026-08-23
8
+
9
+ ### Added
10
+
11
+ - Add motion start/end scheduling and maximum-company discovery-cap configuration to motion API readback and CLI create/update/show workflows.
12
+
13
+ ### Changed
14
+
15
+ - Show conversion totals, stage SLA, and oldest stage age in the default `audienti analytics stages` output so it matches the dashboard read model without requiring `--json`.
16
+
17
+ ## [0.1.36] - 2026-08-22
18
+
19
+ ### Added
20
+
21
+ - Add `audienti lists routing-rules` commands to inspect, create, update, remove, reorder, and apply the same account-scoped list routing rules available in the UI.
22
+
7
23
  ## [0.1.35] - 2026-08-18
8
24
 
9
25
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@ Audienti CLI is the agent-first command-line client for the Audienti production
4
4
  API and the local bridge for Audienti's app-hosted MCP endpoint. It lets local
5
5
  coding agents and operators inspect accounts,
6
6
  create and manage plays, import prospects, build lists, manage task reminders,
7
- and work supported operator flows.
7
+ configure list routing rules, and work supported operator flows.
8
8
 
9
9
  ## Install
10
10
 
@@ -143,6 +143,7 @@ audienti motions quick-start --url https://example.com --wait --confirm
143
143
  audienti motions abm-companies <motn_id> add --file abm-domains.txt
144
144
  audienti motions abm-companies <motn_id> list
145
145
  audienti motions update <motn_id> --status paused
146
+ audienti motions update <motn_id> --start-date 2026-09-01 --end-date 2026-09-30 --maximum-company-count 25
146
147
  audienti motions update <motn_id> --own-post-engagement true
147
148
  audienti motions update <motn_id> --payload motion-signals.json
148
149
  audienti motions activate <motn_id>
@@ -163,6 +164,8 @@ audienti tasks add --title "Review Cristina" --due 2026-07-24T11:00 --prospect <
163
164
  audienti tasks complete <ptsk_id>
164
165
  audienti dnc list
165
166
  audienti company-rules list
167
+ audienti lists routing-rules <list_id> list
168
+ audienti lists routing-rules <list_id> apply
166
169
  audienti users activity --window 7d
167
170
  audienti analytics prospects --window 24h
168
171
  audienti analytics dashboard --play-tag wine_campaign
@@ -201,6 +204,38 @@ audienti tasks add --title "Review target account" --due 2026-07-24T11:00 --list
201
204
  audienti tasks complete <ptsk_id>
202
205
  ```
203
206
 
207
+ To manage the same ordered rules shown in a list's Routing Rules UI, use a JSON
208
+ payload for create and update operations:
209
+
210
+ ```bash
211
+ audienti lists routing-rules <list_id> list
212
+ audienti lists routing-rules <list_id> create --payload routing-rule.json
213
+ audienti lists routing-rules <list_id> update <rule_id> --payload routing-rule.json
214
+ audienti lists routing-rules <list_id> move <rule_id> up
215
+ audienti lists routing-rules <list_id> remove <rule_id>
216
+ audienti lists routing-rules <list_id> apply
217
+ ```
218
+
219
+ ```json
220
+ {
221
+ "name": "VP prospects to Alice",
222
+ "enabled": true,
223
+ "action_kind": "assign_user",
224
+ "target_account_user_id": "alice@example.com",
225
+ "conditions": {
226
+ "seniorities": [
227
+ {"id": "5", "name": "Vice President", "negative": false}
228
+ ],
229
+ "seniority_match_mode": "at_least"
230
+ }
231
+ }
232
+ ```
233
+
234
+ Use `action_kind: "route_to_list"` with `target_list_id` to route to another
235
+ working list. `apply` queues the existing list-routing background job, just like
236
+ the UI; it reports that work was enqueued, not that every prospect has finished
237
+ routing. Run `audienti lists routing-rules help` for the full condition contract.
238
+
204
239
  To inspect activity for prospects that entered the account during a specific
205
240
  cohort while keeping a separate activity window:
206
241
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: audienti
3
- description: Use when the user wants to operate Audienti through the production CLI or app-hosted MCP endpoint, including account selection, plays, prospect imports, lists, message previews, analytics, or supported operator outcomes.
3
+ description: Use when the user wants to operate Audienti through the production CLI or app-hosted MCP endpoint, including account selection, plays, prospect imports, lists and routing rules, message previews, analytics, or supported operator outcomes.
4
4
  ---
5
5
 
6
6
  # Audienti CLI and MCP
@@ -102,15 +102,21 @@ audienti prospects unlock <prsp_id> --json
102
102
  audienti users activity me --window 7d --json
103
103
  audienti prospects import-batch --file prospects.csv --motion <motn_id> --assigned-user me --json
104
104
  audienti lists create --name "Target list" --json
105
+ audienti lists routing-rules <list_id> list --json
106
+ audienti lists routing-rules <list_id> create --payload routing-rule.json --json
107
+ audienti lists routing-rules <list_id> move <rule_id> up --json
108
+ audienti lists routing-rules <list_id> apply --json
105
109
  audienti motions quick-start --url https://example.com --wait --confirm --json
106
110
  audienti motions abm-companies <motn_id> add --file abm-domains.txt --json
107
111
  audienti motions abm-companies <motn_id> list --json
108
112
  audienti motions update <motn_id> --status paused --json
113
+ audienti motions update <motn_id> --start-date 2026-09-01 --end-date 2026-09-30 --maximum-company-count 25 --json
109
114
  audienti motions activate <motn_id> --json
110
115
  audienti motions delete <motn_id> --confirm yes --json
111
116
  audienti operator next --json
112
117
  audienti operator next --plan
113
118
  audienti analytics prospects --window 24h --json
119
+ audienti analytics stages --window 30d
114
120
  audienti analytics visibility --window 24h --user me --json
115
121
  audienti analytics content --window week --json
116
122
  audienti tools list --json
@@ -125,6 +131,19 @@ audienti writer test-run <prsp_id> --mode step --branch no-accept --step 3 --rep
125
131
  audienti writer test-run show <prsp_id> <rprt_id>
126
132
  ```
127
133
 
134
+ List routing-rule create and update commands accept the same normalized
135
+ condition and action data as the list UI. Inspect the ordered rules first, use
136
+ `audienti lists routing-rules help` for the payload contract, and treat `apply`
137
+ as an asynchronous relaunch: it queues the existing routing job and does not
138
+ mean every list member has finished routing.
139
+
140
+ Motion start and end dates are optional lifecycle controls. A due start can
141
+ activate a configured preparing or paused motion; an end date is the final
142
+ active day and pauses the motion afterward. `--maximum-company-count` caps
143
+ durable company admissions only: research and people discovery for admitted
144
+ companies, prospect intake, and automation continue. Use `none` to clear any
145
+ of these settings.
146
+
128
147
  `audienti writer test-run <prsp_id>` starts a report-backed writing session and
129
148
  builds the campaign timeline without drafting every message. The printed
130
149
  `Report: rprt_...` id is the session handle. Pass `--report <rprt_id>` when
package/src/api-client.js CHANGED
@@ -268,6 +268,44 @@ export class AudientiClient {
268
268
  });
269
269
  }
270
270
 
271
+ listRoutingRules(accountId, listId) {
272
+ return this.requestJson(accountPath(accountId, ["lists", listId, "routing_rules"]));
273
+ }
274
+
275
+ createListRoutingRule(accountId, listId, body) {
276
+ return this.requestJson(accountPath(accountId, ["lists", listId, "routing_rules"]), {
277
+ method: "POST",
278
+ body
279
+ });
280
+ }
281
+
282
+ updateListRoutingRule(accountId, listId, ruleId, body) {
283
+ return this.requestJson(accountPath(accountId, ["lists", listId, "routing_rules", ruleId]), {
284
+ method: "PATCH",
285
+ body
286
+ });
287
+ }
288
+
289
+ removeListRoutingRule(accountId, listId, ruleId) {
290
+ return this.requestJson(accountPath(accountId, ["lists", listId, "routing_rules", ruleId]), {
291
+ method: "DELETE"
292
+ });
293
+ }
294
+
295
+ moveListRoutingRule(accountId, listId, ruleId, body) {
296
+ return this.requestJson(accountPath(accountId, ["lists", listId, "routing_rules", ruleId, "move"]), {
297
+ method: "PATCH",
298
+ body
299
+ });
300
+ }
301
+
302
+ applyListRoutingRules(accountId, listId) {
303
+ return this.requestJson(accountPath(accountId, ["lists", listId, "routing_rules", "apply"]), {
304
+ method: "POST",
305
+ body: {}
306
+ });
307
+ }
308
+
271
309
  motions(accountId) {
272
310
  return this.requestJson(accountPath(accountId, ["motions"]));
273
311
  }
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>] | --payload <file.json>) [--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>] [--start-date <YYYY-MM-DD|none>] [--end-date <YYYY-MM-DD|none>] [--maximum-company-count <n|none>] | --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>]";
@@ -99,6 +99,12 @@ const ICPS_ADD_TAG_USAGE = "Usage: audienti icps add-tag <icp_id> <tag> [--json]
99
99
  const ICPS_REMOVE_TAG_USAGE = "Usage: audienti icps remove-tag <icp_id> <tag> [--json] [--account <acct_id>]";
100
100
  const LISTS_ADD_TAG_USAGE = "Usage: audienti lists add-tag <list_id> <tag> [--json] [--account <acct_id>]";
101
101
  const LISTS_REMOVE_TAG_USAGE = "Usage: audienti lists remove-tag <list_id> <tag> [--json] [--account <acct_id>]";
102
+ const LIST_ROUTING_RULES_LIST_USAGE = "Usage: audienti lists routing-rules <list_id> list [--json] [--account <acct_id>]";
103
+ const LIST_ROUTING_RULES_CREATE_USAGE = "Usage: audienti lists routing-rules <list_id> create --payload <file.json> [--json] [--account <acct_id>]";
104
+ const LIST_ROUTING_RULES_UPDATE_USAGE = "Usage: audienti lists routing-rules <list_id> update <rule_id> --payload <file.json> [--json] [--account <acct_id>]";
105
+ const LIST_ROUTING_RULES_REMOVE_USAGE = "Usage: audienti lists routing-rules <list_id> remove <rule_id> [--json] [--account <acct_id>]";
106
+ const LIST_ROUTING_RULES_MOVE_USAGE = "Usage: audienti lists routing-rules <list_id> move <rule_id> <up|down> [--json] [--account <acct_id>]";
107
+ const LIST_ROUTING_RULES_APPLY_USAGE = "Usage: audienti lists routing-rules <list_id> apply [--json] [--account <acct_id>]";
102
108
  const ANALYTICS_PROSPECTS_USAGE = "Usage: audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--list <list_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
103
109
  const ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE = "Usage: audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--list <list_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
104
110
  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>] [--list <list_id>] [--provenance <source>] [--platform <linkedin|email|gmail>] [--json] [--account <acct_id>]";
@@ -237,6 +243,7 @@ async function dispatch(argv, context) {
237
243
  if (normalizedResource === "lists" && action === "prospects") return listProspects(rest, context, { accountOverride });
238
244
  if (normalizedResource === "lists" && action === "add-prospects") return listsAddProspects(rest, context, { accountOverride });
239
245
  if (normalizedResource === "lists" && action === "remove-prospects") return listsRemoveProspects(rest, context, { accountOverride });
246
+ if (normalizedResource === "lists" && ["routing-rules", "rules"].includes(action)) return listsRoutingRules(rest, context, { accountOverride });
240
247
  if (normalizedResource === "motions" && action === "list") return motionsList(rest, context, { accountOverride });
241
248
  if (normalizedResource === "motions" && action === "show") return motionsShow(rest, context, { accountOverride });
242
249
  if (normalizedResource === "motions" && action === "status") return motionsStatus(rest, context, { accountOverride });
@@ -1491,6 +1498,104 @@ async function listsRemoveProspects(args, context, { accountOverride } = {}) {
1491
1498
  return rejected ? 1 : 0;
1492
1499
  }
1493
1500
 
1501
+ async function listsRoutingRules(args, context, { accountOverride } = {}) {
1502
+ const [listId, subaction, ...rest] = args;
1503
+ if (!listId || !subaction) {
1504
+ throw new CommandError("Usage: audienti lists routing-rules <list_id> <list|create|update|remove|move|apply> [args] [--json] [--account <acct_id>]");
1505
+ }
1506
+
1507
+ if (subaction === "list") return listRoutingRulesList(listId, rest, context, { accountOverride });
1508
+ if (subaction === "create") return listRoutingRulesCreate(listId, rest, context, { accountOverride });
1509
+ if (subaction === "update") return listRoutingRulesUpdate(listId, rest, context, { accountOverride });
1510
+ if (["remove", "delete"].includes(subaction)) return listRoutingRulesRemove(listId, rest, context, { accountOverride });
1511
+ if (subaction === "move") return listRoutingRulesMove(listId, rest, context, { accountOverride });
1512
+ if (subaction === "apply") return listRoutingRulesApply(listId, rest, context, { accountOverride });
1513
+
1514
+ throw new CommandError("Usage: audienti lists routing-rules <list_id> <list|create|update|remove|move|apply> [args] [--json] [--account <acct_id>]");
1515
+ }
1516
+
1517
+ async function listRoutingRulesList(listId, args, context, { accountOverride } = {}) {
1518
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1519
+ if (positionals.length > 0) throw new CommandError(LIST_ROUTING_RULES_LIST_USAGE);
1520
+
1521
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1522
+ const payload = await client.listRoutingRules(accountId, listId);
1523
+ if (values.json) return writeJson(context.stdout, payload);
1524
+
1525
+ renderListRoutingRules(payload, context);
1526
+ }
1527
+
1528
+ async function listRoutingRulesCreate(listId, args, context, { accountOverride } = {}) {
1529
+ const { values, positionals } = parseCommandArgs(args, {
1530
+ ...jsonOptions(),
1531
+ payload: {type: "string"}
1532
+ });
1533
+ if (positionals.length > 0 || !values.payload) throw new CommandError(LIST_ROUTING_RULES_CREATE_USAGE);
1534
+
1535
+ const ruleInput = await readJsonPayload(values.payload);
1536
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1537
+ const payload = await client.createListRoutingRule(accountId, listId, {routing_rule: ruleInput});
1538
+ if (values.json) return writeJson(context.stdout, payload);
1539
+
1540
+ const rule = payload?.routing_rule;
1541
+ writeLine(context.stdout, `Created routing rule ${display(rule?.name)} (${display(rule?.id)}) on list ${display(payload?.list_id || listId)}.`);
1542
+ }
1543
+
1544
+ async function listRoutingRulesUpdate(listId, args, context, { accountOverride } = {}) {
1545
+ const { values, positionals } = parseCommandArgs(args, {
1546
+ ...jsonOptions(),
1547
+ payload: {type: "string"}
1548
+ });
1549
+ if (positionals.length !== 1 || !values.payload) throw new CommandError(LIST_ROUTING_RULES_UPDATE_USAGE);
1550
+
1551
+ const ruleInput = await readJsonPayload(values.payload);
1552
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1553
+ const payload = await client.updateListRoutingRule(accountId, listId, positionals[0], {routing_rule: ruleInput});
1554
+ if (values.json) return writeJson(context.stdout, payload);
1555
+
1556
+ const rule = payload?.routing_rule;
1557
+ writeLine(context.stdout, `Updated routing rule ${display(rule?.name)} (${display(rule?.id)}) on list ${display(payload?.list_id || listId)}.`);
1558
+ }
1559
+
1560
+ async function listRoutingRulesRemove(listId, args, context, { accountOverride } = {}) {
1561
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1562
+ if (positionals.length !== 1) throw new CommandError(LIST_ROUTING_RULES_REMOVE_USAGE);
1563
+
1564
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1565
+ const payload = await client.removeListRoutingRule(accountId, listId, positionals[0]);
1566
+ if (values.json) return writeJson(context.stdout, payload);
1567
+
1568
+ const rule = payload?.routing_rule;
1569
+ writeLine(context.stdout, `Removed routing rule ${display(rule?.name)} (${display(rule?.id || positionals[0])}) from list ${display(listId)}.`);
1570
+ }
1571
+
1572
+ async function listRoutingRulesMove(listId, args, context, { accountOverride } = {}) {
1573
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1574
+ const [ruleId, direction] = positionals;
1575
+ if (positionals.length !== 2 || !["up", "down"].includes(direction)) {
1576
+ throw new CommandError(LIST_ROUTING_RULES_MOVE_USAGE);
1577
+ }
1578
+
1579
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1580
+ const payload = await client.moveListRoutingRule(accountId, listId, ruleId, {direction});
1581
+ if (values.json) return writeJson(context.stdout, payload);
1582
+
1583
+ const verb = payload?.moved ? "Moved" : "Could not move";
1584
+ writeLine(context.stdout, `${verb} routing rule ${display(ruleId)} ${direction} on list ${display(payload?.list_id || listId)}.`);
1585
+ renderListRoutingRules(payload, context);
1586
+ }
1587
+
1588
+ async function listRoutingRulesApply(listId, args, context, { accountOverride } = {}) {
1589
+ const { values, positionals } = parseCommandArgs(args, jsonOptions());
1590
+ if (positionals.length > 0) throw new CommandError(LIST_ROUTING_RULES_APPLY_USAGE);
1591
+
1592
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1593
+ const payload = await client.applyListRoutingRules(accountId, listId);
1594
+ if (values.json) return writeJson(context.stdout, payload);
1595
+
1596
+ writeLine(context.stdout, `Routing rules will be applied in the background to list ${display(payload?.list_id || listId)}.`);
1597
+ }
1598
+
1494
1599
  async function motionsList(args, context, { accountOverride } = {}) {
1495
1600
  const { values, positionals } = parseCommandArgs(args, {
1496
1601
  ...jsonOptions(),
@@ -1776,7 +1881,10 @@ async function motionsUpdate(args, context, { accountOverride } = {}) {
1776
1881
  payload: { type: "string" },
1777
1882
  status: { type: "string" },
1778
1883
  tags: { type: "string" },
1779
- "own-post-engagement": { type: "string" }
1884
+ "own-post-engagement": { type: "string" },
1885
+ "start-date": { type: "string" },
1886
+ "end-date": { type: "string" },
1887
+ "maximum-company-count": { type: "string" }
1780
1888
  });
1781
1889
  const hasUpdateField = values.payload || motionSimpleFieldsPresent(values);
1782
1890
  if (positionals.length !== 1 || !hasUpdateField) {
@@ -1794,7 +1902,7 @@ async function motionsUpdate(args, context, { accountOverride } = {}) {
1794
1902
  async function motionUpdatePayload(values) {
1795
1903
  if (values.payload) {
1796
1904
  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.");
1905
+ throw new CommandError("Choose one motion input mode: either --payload <file.json> or the simple motion update flags.");
1798
1906
  }
1799
1907
 
1800
1908
  const payload = await readJsonPayload(values.payload);
@@ -1802,15 +1910,54 @@ async function motionUpdatePayload(values) {
1802
1910
  return payload;
1803
1911
  }
1804
1912
 
1805
- return compactObject({
1913
+ const payload = compactObject({
1806
1914
  status: normalizeMotionStatus(values.status),
1807
1915
  play_tags: values.tags !== undefined ? tagList(values.tags) : undefined,
1808
1916
  own_post_engagement: values["own-post-engagement"] !== undefined ? parseBooleanString(values["own-post-engagement"], "--own-post-engagement") : undefined
1809
1917
  });
1918
+ const nullableSettings = {
1919
+ starts_on: normalizeMotionDate(values["start-date"], "--start-date"),
1920
+ ends_on: normalizeMotionDate(values["end-date"], "--end-date"),
1921
+ maximum_company_count: normalizeMotionMaximumCompanyCount(values["maximum-company-count"])
1922
+ };
1923
+ for (const [key, value] of Object.entries(nullableSettings)) {
1924
+ if (value !== undefined) payload[key] = value;
1925
+ }
1926
+
1927
+ return payload;
1810
1928
  }
1811
1929
 
1812
1930
  function motionSimpleFieldsPresent(values) {
1813
- return Boolean(values.status) || values.tags !== undefined || values["own-post-engagement"] !== undefined;
1931
+ return Boolean(values.status) ||
1932
+ values.tags !== undefined ||
1933
+ values["own-post-engagement"] !== undefined ||
1934
+ values["start-date"] !== undefined ||
1935
+ values["end-date"] !== undefined ||
1936
+ values["maximum-company-count"] !== undefined;
1937
+ }
1938
+
1939
+ function normalizeMotionDate(value, flagName) {
1940
+ if (value === undefined) return undefined;
1941
+
1942
+ const normalized = String(value || "").trim().toLowerCase();
1943
+ if (normalized === "none") return null;
1944
+ const match = normalized.match(/^(\d{4})-(\d{2})-(\d{2})$/);
1945
+ if (!match) throw new CommandError(`${flagName} must be YYYY-MM-DD or none.`);
1946
+
1947
+ const [, year, month, day] = match.map(Number);
1948
+ const parsed = new Date(Date.UTC(year, month - 1, day));
1949
+ if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
1950
+ throw new CommandError(`${flagName} must be YYYY-MM-DD or none.`);
1951
+ }
1952
+
1953
+ return normalized;
1954
+ }
1955
+
1956
+ function normalizeMotionMaximumCompanyCount(value) {
1957
+ if (value === undefined) return undefined;
1958
+ if (String(value || "").trim().toLowerCase() === "none") return null;
1959
+
1960
+ return normalizeOptionalPositiveInteger(value, "--maximum-company-count");
1814
1961
  }
1815
1962
 
1816
1963
  function validateMotionSignalRows(payload) {
@@ -4132,6 +4279,30 @@ function renderList(list, context) {
4132
4279
  if (list?.description) writeLine(context.stdout, `Description: ${list.description}`);
4133
4280
  }
4134
4281
 
4282
+ function renderListRoutingRules(payload, context) {
4283
+ const rules = Array.isArray(payload?.routing_rules) ? payload.routing_rules : [];
4284
+ if (rules.length === 0) return writeLine(context.stdout, "No routing rules found.");
4285
+
4286
+ writeLine(context.stdout, "RULE ID\tPOSITION\tENABLED\tACTION\tTARGET\tNAME");
4287
+ for (const rule of rules) {
4288
+ writeLine(context.stdout, [
4289
+ display(rule.id),
4290
+ display(rule.position),
4291
+ rule.enabled ? "yes" : "no",
4292
+ display(rule.action_kind),
4293
+ routingRuleTargetLabel(rule),
4294
+ display(rule.name)
4295
+ ].join("\t"));
4296
+ }
4297
+ }
4298
+
4299
+ function routingRuleTargetLabel(rule) {
4300
+ return rule?.target_account_user?.name ||
4301
+ rule?.target_account_user?.email ||
4302
+ rule?.target_list?.name ||
4303
+ "-";
4304
+ }
4305
+
4135
4306
  function renderTags(tags, context) {
4136
4307
  if (!Array.isArray(tags) || tags.length === 0) return writeLine(context.stdout, "No tags found.");
4137
4308
 
@@ -4459,6 +4630,9 @@ function renderMotion(motion, context) {
4459
4630
  writeLine(context.stdout, `Motion: ${display(motion?.name)} (${display(motion?.prefix_id)})`);
4460
4631
  writeLine(context.stdout, `Status: ${display(motion?.status)}`);
4461
4632
  writeLine(context.stdout, `Kind: ${display(motion?.kind)}`);
4633
+ writeLine(context.stdout, `Start date: ${display(motion?.starts_on, "not set")}`);
4634
+ writeLine(context.stdout, `End date: ${display(motion?.ends_on, "not set")}`);
4635
+ writeLine(context.stdout, `Maximum companies: ${display(motion?.maximum_company_count, "not set")}`);
4462
4636
  renderExecutableConfiguration(motion?.executable_configuration, context);
4463
4637
  if (motion?.offer?.name) writeLine(context.stdout, `Offer: ${motion.offer.name} (${display(motion.offer.prefix_id)})`);
4464
4638
  if (motion?.icp?.name) writeLine(context.stdout, `ICP: ${motion.icp.name} (${display(motion.icp.prefix_id)})`);
@@ -5651,6 +5825,7 @@ function writeStageConversionTable(conversionGrid, context) {
5651
5825
  const rows = Array.isArray(conversionGrid?.rows) ? conversionGrid.rows : [];
5652
5826
  const definitions = Array.isArray(conversionGrid?.stage_definitions) ? conversionGrid.stage_definitions : [];
5653
5827
  const columns = definitions.filter((definition) => definition?.key);
5828
+ const totalValues = conversionGrid?.total_values || {};
5654
5829
 
5655
5830
  writeLine(context.stdout, "");
5656
5831
  writeLine(context.stdout, display(conversionGrid?.window_description, "Conversion cohorts"));
@@ -5661,6 +5836,12 @@ function writeStageConversionTable(conversionGrid, context) {
5661
5836
  display(row?.label),
5662
5837
  ...columns.map((column) => stageMetricValueLabel(row?.values?.[column.key]))
5663
5838
  ]);
5839
+ if (Object.keys(totalValues).length > 0) {
5840
+ tableRows.push([
5841
+ "TOTAL",
5842
+ ...columns.map((column) => stageMetricValueLabel(totalValues[column.key]))
5843
+ ]);
5844
+ }
5664
5845
 
5665
5846
  writeAlignedTable(context, headers, tableRows);
5666
5847
  }
@@ -5678,20 +5859,22 @@ function writeStageAgingTable(stageAging, context) {
5678
5859
  );
5679
5860
  writeAlignedTable(
5680
5861
  context,
5681
- ["STAGE", "CURRENT", "DUE SOON", "OVERDUE", "OVERDUE %", "MED AGE", "OLDEST IDLE"],
5862
+ ["STAGE", "SLA", "CURRENT", "DUE SOON", "OVERDUE", "OVERDUE %", "MED AGE", "OLDEST AGE", "OLDEST IDLE"],
5682
5863
  rows.map(stageAgingRow),
5683
- { numericColumns: [false, true, true, true, true, true, true] }
5864
+ { numericColumns: [false, true, true, true, true, true, true, true, true] }
5684
5865
  );
5685
5866
  }
5686
5867
 
5687
5868
  function stageAgingRow(row) {
5688
5869
  return [
5689
5870
  display(row?.label || row?.key),
5871
+ dayCountLabel(row?.due_after_days),
5690
5872
  display(row?.current_count, 0),
5691
5873
  display(row?.due_soon_count, 0),
5692
5874
  display(row?.overdue_count, 0),
5693
5875
  percentageLabel(row?.overdue_rate),
5694
5876
  dayCountLabel(row?.median_stage_age_days),
5877
+ dayCountLabel(row?.oldest_stage_age_days),
5695
5878
  dayCountLabel(row?.oldest_idle_days)
5696
5879
  ];
5697
5880
  }
@@ -6447,7 +6630,7 @@ const HELP_TOPICS = new Map([
6447
6630
  " audienti motions abm-companies <motn_id> list",
6448
6631
  " audienti motions abm-companies <motn_id> add <domain_or_linkedin_url>...",
6449
6632
  " audienti motions create --payload <file.json>",
6450
- " audienti motions update <motn_id> [--status <state>] [--tags <tag[,tag...]>] [--own-post-engagement <true|false>]",
6633
+ " audienti motions update <motn_id> [--status <state>] [--tags <tag[,tag...]>] [--own-post-engagement <true|false>] [--start-date <date|none>] [--end-date <date|none>] [--maximum-company-count <n|none>]",
6451
6634
  " audienti motions update <motn_id> --payload <file.json>",
6452
6635
  " audienti motions add-tag <motn_id> <tag>",
6453
6636
  " audienti motions remove-tag <motn_id> <tag>",
@@ -6492,6 +6675,9 @@ const HELP_TOPICS = new Map([
6492
6675
  " audienti lists prospects <list_id>",
6493
6676
  " audienti lists add-tag <list_id> <tag>",
6494
6677
  " audienti lists remove-tag <list_id> <tag>",
6678
+ " audienti lists routing-rules <list_id> list",
6679
+ " audienti lists routing-rules <list_id> create --payload <file.json>",
6680
+ " audienti lists routing-rules <list_id> apply",
6495
6681
  " audienti tags list",
6496
6682
  " audienti tags show <tag>",
6497
6683
  " audienti tasks list [--status open]",
@@ -7472,13 +7658,63 @@ const HELP_TOPICS = new Map([
7472
7658
  " audienti lists prospects <list_id> [--json]",
7473
7659
  " audienti lists add-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
7474
7660
  " audienti lists remove-prospects <list_id> <prsp_id> [prsp_id...] [--json]",
7661
+ " audienti lists routing-rules <list_id> <list|create|update|remove|move|apply> [args] [--json]",
7475
7662
  "",
7476
- "Status: read, create, update, delete, and membership commands implemented",
7663
+ "Status: read, create, update, delete, membership, and routing-rule commands implemented",
7477
7664
  "",
7478
7665
  "ID shape:",
7479
7666
  " list_id: list_ prefix id"
7480
7667
  ].join("\n")],
7481
7668
 
7669
+ ["lists routing-rules", [
7670
+ "Usage:",
7671
+ " audienti lists routing-rules <list_id> list [--json] [--account <acct_id>]",
7672
+ " audienti lists routing-rules <list_id> create --payload <file.json> [--json] [--account <acct_id>]",
7673
+ " audienti lists routing-rules <list_id> update <rule_id> --payload <file.json> [--json] [--account <acct_id>]",
7674
+ " audienti lists routing-rules <list_id> remove <rule_id> [--json] [--account <acct_id>]",
7675
+ " audienti lists routing-rules <list_id> move <rule_id> <up|down> [--json] [--account <acct_id>]",
7676
+ " audienti lists routing-rules <list_id> apply [--json] [--account <acct_id>]",
7677
+ "",
7678
+ "Status: implemented",
7679
+ "",
7680
+ "Purpose:",
7681
+ " Configure, inspect, reorder, and relaunch the same routing rules available in the list UI.",
7682
+ " Apply only queues the existing background job; it does not wait for routing to finish.",
7683
+ "",
7684
+ "Payload shape:",
7685
+ " name: string",
7686
+ " enabled: boolean | optional",
7687
+ " position: non-negative integer | optional",
7688
+ " action_kind: assign_user | route_to_list",
7689
+ " target_account_user_id: account-user id, email, or me | required for assign_user",
7690
+ " target_list_id: list_ prefix id or numeric id | required for route_to_list",
7691
+ " conditions: object | optional",
7692
+ "",
7693
+ "Condition keys:",
7694
+ " locations, company_locations, seniorities, job_titles, bio_texts,",
7695
+ " company_sizes, company_types, company_keywords, seniority_match_mode, industry_groups",
7696
+ "",
7697
+ "Example payload file:",
7698
+ " {",
7699
+ " \"name\": \"VP prospects to Alice\",",
7700
+ " \"enabled\": true,",
7701
+ " \"action_kind\": \"assign_user\",",
7702
+ " \"target_account_user_id\": \"alice@example.com\",",
7703
+ " \"conditions\": {",
7704
+ " \"seniorities\": [{\"id\": \"5\", \"name\": \"Vice President\", \"negative\": false}],",
7705
+ " \"seniority_match_mode\": \"at_least\"",
7706
+ " }",
7707
+ " }",
7708
+ "",
7709
+ "API:",
7710
+ " GET /api/v1/accounts/:account_id/lists/:list_id/routing_rules.json",
7711
+ " POST /api/v1/accounts/:account_id/lists/:list_id/routing_rules.json",
7712
+ " PATCH /api/v1/accounts/:account_id/lists/:list_id/routing_rules/:id.json",
7713
+ " DELETE /api/v1/accounts/:account_id/lists/:list_id/routing_rules/:id.json",
7714
+ " PATCH /api/v1/accounts/:account_id/lists/:list_id/routing_rules/:id/move.json",
7715
+ " POST /api/v1/accounts/:account_id/lists/:list_id/routing_rules/apply.json"
7716
+ ].join("\n")],
7717
+
7482
7718
  ["lists list", [
7483
7719
  "Usage:",
7484
7720
  " audienti lists list [--tag <tag>] [--json] [--account <acct_id>]",
@@ -7717,7 +7953,7 @@ const HELP_TOPICS = new Map([
7717
7953
  " audienti motions abm-companies <motn_id> add (<domain_or_linkedin_url>... | --file <txt|json>) [--json]",
7718
7954
  " audienti motions abm-companies <motn_id> remove <row_id> [--json]",
7719
7955
  " audienti motions create --payload <file.json> [--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]",
7956
+ ` ${MOTIONS_UPDATE_USAGE.slice("Usage: ".length).replace(" [--account <acct_id>]", "")}`,
7721
7957
  " audienti motions add-tag <motn_id> <tag> [--json]",
7722
7958
  " audienti motions remove-tag <motn_id> <tag> [--json]",
7723
7959
  " audienti motions activate <motn_id> [--json]",
@@ -7995,6 +8231,9 @@ const HELP_TOPICS = new Map([
7995
8231
  " icp_id: icpp_ prefix id | optional",
7996
8232
  " list_id: list_ prefix id | optional",
7997
8233
  " play_tags: [string] | optional",
8234
+ " starts_on: YYYY-MM-DD | null | optional scheduled activation date",
8235
+ " ends_on: YYYY-MM-DD | null | optional final active date",
8236
+ " maximum_company_count: positive integer | null | optional new-company discovery cap",
7998
8237
  " 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
8238
  " signal_questions: newline-delimited company::, person::, or both:: legacy question text | optional for outbound motions",
8000
8239
  " inbound_channels: [linkedin | reddit] | optional; defaults to [linkedin] for inbound motions",
@@ -8009,6 +8248,9 @@ const HELP_TOPICS = new Map([
8009
8248
  " \"offer_id\": \"offr_abc123\",",
8010
8249
  " \"principal_account_user_id\": 42,",
8011
8250
  " \"list_id\": \"list_abc123\",",
8251
+ " \"starts_on\": \"2026-09-01\",",
8252
+ " \"ends_on\": \"2026-09-30\",",
8253
+ " \"maximum_company_count\": 25,",
8012
8254
  " \"play_tags\": [\"sarit\", \"pj\"]",
8013
8255
  " }",
8014
8256
  "",
@@ -8032,8 +8274,8 @@ const HELP_TOPICS = new Map([
8032
8274
  " name: new motion name",
8033
8275
  "",
8034
8276
  "Behavior:",
8035
- " Copies the motion kind, offer, ICP, principal, premise, approach, targeting profile, suppression policy, secondary roles, and active signal rows.",
8036
- " The clone starts as draft with a new empty backing list.",
8277
+ " Copies the motion kind, offer, ICP, principal, premise, approach, maximum company count, targeting profile, suppression policy, secondary roles, and active signal rows.",
8278
+ " The clone starts as draft with a new empty backing list and no inherited schedule dates.",
8037
8279
  "",
8038
8280
  "API:",
8039
8281
  " POST /api/v1/accounts/:account_id/motions/:id/clone.json",
@@ -8059,10 +8301,14 @@ const HELP_TOPICS = new Map([
8059
8301
  " motn_id: motn_ prefix id",
8060
8302
  " status: draft | preparing | active | paused | archived | optional",
8061
8303
  " tags: comma-separated tag list | optional",
8304
+ " start-date: YYYY-MM-DD | none | optional",
8305
+ " end-date: YYYY-MM-DD | none | optional",
8306
+ " maximum-company-count: positive integer | none | optional",
8062
8307
  " payload: file.json | optional full or partial motion object using the account API shape",
8063
8308
  "",
8064
8309
  "Behavior:",
8065
- " Choose either --payload or simple flags. Updates only the provided fields. Sending --tags replaces the motion's full tag set.",
8310
+ " Choose either --payload or simple flags. Updates only the provided fields. Use none to clear a schedule date or company cap. Sending --tags replaces the motion's full tag set.",
8311
+ " The company cap stops only new company discovery; existing company research, people discovery, prospect intake, and automation continue.",
8066
8312
  " Payload mode supports outbound signal_rows or legacy signal_questions; supplied rows replace the motion's active ready signals.",
8067
8313
  " posting_language is only supported on company-scope hiring signal rows; use topics for person discussion evidence.",
8068
8314
  "",