@audienti/cli 0.1.78 → 0.1.80

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,20 @@ All notable changes to the Audienti CLI are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.80] - 2026-09-28
8
+
9
+ ### Added
10
+
11
+ - Run the motion list and motion health actions from the CLI: `motions bulk-add-tag --tag`, `bulk-remove-tag --tag`, `bulk-update-principal --principal`, `bulk-update-status --status` (prints any motion the lifecycle rules blocked), `retire-strategy --strategy`, `refresh-launch-check`, `refresh-launch-checks`, and `update-premise --premise`.
12
+
13
+ ## [0.1.79] - 2026-09-28
14
+
15
+ ### Added
16
+
17
+ - Run the remaining prospect page actions from the CLI: `prospects defer`, `delay --for`, `monitor`, `unmonitor`, `rename --name`, `import-post --url`, `sync --social-cookie`, `cancel-event --event`, `queue-draft` (optional `--context` JSON object), `rewrite`, `engage --type` (with `--post`, `--comment`, `--reply-to`, `--request-event`, `--queue-action` and `--principal` targets), `reject-selected`, and `intake --url`.
18
+ - Add `events retry`, `profiles delete`, `companies stop-pursuing`, `content defer --for`, `network-ops adopt --target-account --motion`, `network-ops ignore`, `reconciliations add-to-motion --motion`, and `reconciliations ignore`.
19
+ - Add `inbox-ops update-filters`, `inbox-ops draft-reply`, `inbox-ops reply --message`, and `inbox-ops adopt`. Each reply sends a fresh reply token so a retried command cannot send twice.
20
+
7
21
  ## [0.1.78] - 2026-09-27
8
22
 
9
23
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api-client.js CHANGED
@@ -675,6 +675,59 @@ export class AudientiClient {
675
675
  });
676
676
  }
677
677
 
678
+ bulkAddMotionTag(accountId, body) {
679
+ return this.requestJson(accountPath(accountId, ["motions", "bulk_add_tag"]), {
680
+ method: "POST",
681
+ body
682
+ });
683
+ }
684
+
685
+ bulkRemoveMotionTag(accountId, body) {
686
+ return this.requestJson(accountPath(accountId, ["motions", "bulk_remove_tag"]), {
687
+ method: "POST",
688
+ body
689
+ });
690
+ }
691
+
692
+ bulkUpdateMotionPrincipal(accountId, body) {
693
+ return this.requestJson(accountPath(accountId, ["motions", "bulk_update_principal"]), {
694
+ method: "POST",
695
+ body
696
+ });
697
+ }
698
+
699
+ bulkUpdateMotionStatus(accountId, body) {
700
+ return this.requestJson(accountPath(accountId, ["motions", "bulk_update_status"]), {
701
+ method: "POST",
702
+ body
703
+ });
704
+ }
705
+
706
+ retireMotionStrategy(accountId, motionId, strategyId) {
707
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "strategies", strategyId, "retire"]), {
708
+ method: "POST"
709
+ });
710
+ }
711
+
712
+ refreshMotionLaunchCheck(accountId, motionId) {
713
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "refresh_launch_check"]), {
714
+ method: "POST"
715
+ });
716
+ }
717
+
718
+ refreshMotionLaunchChecks(accountId) {
719
+ return this.requestJson(accountPath(accountId, ["motions", "refresh_launch_checks"]), {
720
+ method: "POST"
721
+ });
722
+ }
723
+
724
+ updateMotionPremise(accountId, motionId, body) {
725
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "update_premise"]), {
726
+ method: "PATCH",
727
+ body
728
+ });
729
+ }
730
+
678
731
  createQuickStart(accountId, body) {
679
732
  return this.requestJson(accountPath(accountId, ["quick_start"]), {
680
733
  method: "POST",
@@ -794,6 +847,160 @@ export class AudientiClient {
794
847
  });
795
848
  }
796
849
 
850
+ deferProspect(accountId, prospectId) {
851
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "defer"]), {
852
+ method: "POST"
853
+ });
854
+ }
855
+
856
+ delayProspect(accountId, prospectId, body = {}) {
857
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "delay"]), {
858
+ method: "POST",
859
+ body
860
+ });
861
+ }
862
+
863
+ monitorProspect(accountId, prospectId, body = {}) {
864
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "monitor"]), {
865
+ method: "POST",
866
+ body
867
+ });
868
+ }
869
+
870
+ unmonitorProspect(accountId, prospectId) {
871
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "unmonitor"]), {
872
+ method: "POST"
873
+ });
874
+ }
875
+
876
+ renameProspect(accountId, prospectId, body = {}) {
877
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "display_name"]), {
878
+ method: "PATCH",
879
+ body
880
+ });
881
+ }
882
+
883
+ importProspectPost(accountId, prospectId, body = {}) {
884
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "import_post"]), {
885
+ method: "POST",
886
+ body
887
+ });
888
+ }
889
+
890
+ syncProspect(accountId, prospectId, body = {}) {
891
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "sync"]), {
892
+ method: "POST",
893
+ body
894
+ });
895
+ }
896
+
897
+ cancelProspectScheduledEvent(accountId, prospectId, body = {}) {
898
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "cancel_scheduled_event"]), {
899
+ method: "POST",
900
+ body
901
+ });
902
+ }
903
+
904
+ writeProspectQueueDraft(accountId, prospectId, body = {}) {
905
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "write"]), {
906
+ method: "POST",
907
+ body
908
+ });
909
+ }
910
+
911
+ rewriteProspectQueueDraft(accountId, prospectId, body = {}) {
912
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "rewrite"]), {
913
+ method: "POST",
914
+ body
915
+ });
916
+ }
917
+
918
+ engageProspect(accountId, prospectId, body = {}) {
919
+ return this.requestJson(accountPath(accountId, ["prospects", prospectId, "engage"]), {
920
+ method: "POST",
921
+ body
922
+ });
923
+ }
924
+
925
+ rejectSelectedProspects(accountId, body = {}) {
926
+ return this.requestJson(accountPath(accountId, ["prospects", "reject_selected"]), {
927
+ method: "POST",
928
+ body
929
+ });
930
+ }
931
+
932
+ intakeProspect(accountId, body = {}) {
933
+ return this.requestJson(accountPath(accountId, ["prospects", "intake"]), {
934
+ method: "POST",
935
+ body
936
+ });
937
+ }
938
+
939
+ retryEvent(accountId, eventId) {
940
+ return this.requestJson(accountPath(accountId, ["events", eventId, "retry"]), {
941
+ method: "POST"
942
+ });
943
+ }
944
+
945
+ deleteProfile(accountId, profileId) {
946
+ return this.requestJson(accountPath(accountId, ["profiles", profileId]), {
947
+ method: "DELETE"
948
+ });
949
+ }
950
+
951
+ stopPursuingCompany(accountId, companyId) {
952
+ return this.requestJson(accountPath(accountId, ["companies", companyId, "stop_pursuing"]), {
953
+ method: "POST"
954
+ });
955
+ }
956
+
957
+ contentDefer(accountId, workItemId, body = {}) {
958
+ return this.requestJson(accountPath(accountId, ["content_ops", "work_items", workItemId, "defer"]), {
959
+ method: "POST",
960
+ body
961
+ });
962
+ }
963
+
964
+ updateInboxOpsFilters(accountId, body = {}) {
965
+ return this.requestJson(accountPath(accountId, ["inbox_ops", "filters"]), {
966
+ method: "PATCH",
967
+ body
968
+ });
969
+ }
970
+
971
+ inboxOpsReply(accountId, rowId, body = {}) {
972
+ return this.requestJson(accountPath(accountId, ["inbox_ops", rowId, "reply"]), {
973
+ method: "POST",
974
+ body
975
+ });
976
+ }
977
+
978
+ inboxOpsWriteReply(accountId, rowId) {
979
+ return this.requestJson(accountPath(accountId, ["inbox_ops", rowId, "reply", "write"]), {
980
+ method: "POST"
981
+ });
982
+ }
983
+
984
+ inboxOpsAdopt(accountId, rowId, body = {}) {
985
+ return this.requestJson(accountPath(accountId, ["inbox_ops", rowId, "adopt"]), {
986
+ method: "POST",
987
+ body
988
+ });
989
+ }
990
+
991
+ reconciliationAddToMotion(accountId, sourceKey, body = {}) {
992
+ return this.requestJson(accountPath(accountId, ["reconciliations", sourceKey, "add_to_motion"]), {
993
+ method: "POST",
994
+ body
995
+ });
996
+ }
997
+
998
+ reconciliationIgnore(accountId, sourceKey) {
999
+ return this.requestJson(accountPath(accountId, ["reconciliations", sourceKey, "ignore"]), {
1000
+ method: "POST"
1001
+ });
1002
+ }
1003
+
797
1004
  prospectSequencePreview(accountId, prospectId, body = {}) {
798
1005
  return this.requestJson(accountPath(accountId, ["prospects", prospectId, "sequence_preview"]), {
799
1006
  method: "POST",
@@ -883,9 +1090,10 @@ export class AudientiClient {
883
1090
  return this.requestJson(accountPath(accountId, ["operator"], query));
884
1091
  }
885
1092
 
886
- networkOpsAction(accountId, rowId, action) {
1093
+ networkOpsAction(accountId, rowId, action, body) {
887
1094
  return this.requestJson(accountPath(accountId, ["network_ops", rowId, action]), {
888
- method: "POST"
1095
+ method: "POST",
1096
+ ...(body === undefined ? {} : { body })
889
1097
  });
890
1098
  }
891
1099
 
package/src/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { createServer } from "node:http";
3
3
  import { spawn } from "node:child_process";
4
- import { randomBytes } from "node:crypto";
4
+ import { randomBytes, randomUUID } from "node:crypto";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import { basename, join } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
@@ -309,6 +309,8 @@ async function dispatch(argv, context) {
309
309
 
310
310
  const [resource, action, ...rest] = args;
311
311
  const normalizedResource = normalizeResource(resource);
312
+ const parityActionTopic = `${normalizedResource} ${action}`;
313
+ if (PARITY_ACTION_COMMANDS.has(parityActionTopic)) return runParityActionCommand(parityActionTopic, rest, context, { accountOverride });
312
314
 
313
315
  if (normalizedResource === "auth" && action === "token") return authToken(rest, context);
314
316
  if (normalizedResource === "auth" && action === "login") return authLogin(rest, context);
@@ -3335,10 +3337,21 @@ async function motionsUpdate(args, context, { accountOverride } = {}) {
3335
3337
 
3336
3338
  const { client, accountId } = await requireAccountContext(context, { accountOverride });
3337
3339
  const motion = await client.updateMotion(accountId, positionals[0], { motion: await motionUpdatePayload(values) });
3338
- if (values.json) return writeJson(context.stdout, motion);
3340
+ const statusBlocked = motion?.status_change?.status === "blocked";
3341
+ if (values.json) {
3342
+ writeJson(context.stdout, motion);
3343
+ return statusBlocked ? 1 : undefined;
3344
+ }
3339
3345
 
3340
3346
  writeLine(context.stdout, `Updated motion ${display(motion?.name)} (${display(motion?.prefix_id)}).`);
3341
3347
  renderMotion(motion, context);
3348
+ if (!statusBlocked) return undefined;
3349
+
3350
+ writeLine(
3351
+ context.stderr,
3352
+ `Status not changed to ${display(motion.status_change.requested_status)}: ${display(motion.status_change.reason)}. Other changes were saved.`
3353
+ );
3354
+ return 1;
3342
3355
  }
3343
3356
 
3344
3357
  async function motionUpdatePayload(values) {
@@ -14056,3 +14069,424 @@ const HELP_TOPICS = new Map([
14056
14069
  " Operator outcome writeback is implemented for prospect rows, not visibility rows."
14057
14070
  ].join("\n")]
14058
14071
  ]);
14072
+
14073
+ // Single-purpose actions that mirror one web button each (#2300, #2302).
14074
+ // Every entry calls one API route that shares its service with the web action.
14075
+ const ACCOUNT_OPTION_USAGE = "[--json] [--account <acct_id>]";
14076
+ const PARITY_ACTION_COMMANDS = new Map([
14077
+ ["prospects defer", {
14078
+ usage: "audienti prospects defer <prsp_id>",
14079
+ purpose: "Push one prospect out of the queue for 24 hours, like Defer in the queue.",
14080
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/defer.json",
14081
+ run: (client, accountId, [id]) => client.deferProspect(accountId, id),
14082
+ done: ([id]) => `Deferred prospect ${id} for 24 hours.`
14083
+ }],
14084
+ ["prospects delay", {
14085
+ usage: "audienti prospects delay <prsp_id> --for <1_day|1_week|1_month|3_months|6_months>",
14086
+ purpose: "Delay one prospect and move it to the Delayed list until the window ends.",
14087
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/delay.json",
14088
+ options: { for: { type: "string" } },
14089
+ required: ["for"],
14090
+ run: (client, accountId, [id], values) => client.delayProspect(accountId, id, { delay_duration: values.for }),
14091
+ done: ([id], payload) => `Delayed prospect ${id} until ${display(payload?.undelay_at)}.`
14092
+ }],
14093
+ ["prospects monitor", {
14094
+ usage: "audienti prospects monitor <prsp_id> [--note <text>]",
14095
+ purpose: "Move one prospect to monitoring, like Monitor on the prospect page.",
14096
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/monitor.json",
14097
+ options: { note: { type: "string" } },
14098
+ run: (client, accountId, [id], values) => client.monitorProspect(accountId, id, compactObject({ monitor_note: values.note })),
14099
+ done: ([id]) => `Monitoring prospect ${id}.`
14100
+ }],
14101
+ ["prospects unmonitor", {
14102
+ usage: "audienti prospects unmonitor <prsp_id>",
14103
+ purpose: "Stop monitoring one prospect.",
14104
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/unmonitor.json",
14105
+ run: (client, accountId, [id]) => client.unmonitorProspect(accountId, id),
14106
+ done: ([id]) => `Stopped monitoring prospect ${id}.`
14107
+ }],
14108
+ ["prospects rename", {
14109
+ usage: "audienti prospects rename <prsp_id> --name <display_name>",
14110
+ purpose: "Change a prospect's display name. Backend admins only, like the web action.",
14111
+ api: "PATCH /api/v1/accounts/:account_id/prospects/:id/display_name.json",
14112
+ options: { name: { type: "string" } },
14113
+ required: ["name"],
14114
+ run: (client, accountId, [id], values) => client.renameProspect(accountId, id, { display_name: values.name }),
14115
+ done: ([id]) => `Renamed prospect ${id}.`
14116
+ }],
14117
+ ["prospects import-post", {
14118
+ usage: "audienti prospects import-post <prsp_id> --url <post_url> [--topic <topic_id>]",
14119
+ purpose: "Import one LinkedIn post for a prospect in the background.",
14120
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/import_post.json",
14121
+ options: { url: { type: "string" }, topic: { type: "string" } },
14122
+ required: ["url"],
14123
+ run: (client, accountId, [id], values) => client.importProspectPost(accountId, id, compactObject({ url: values.url, topic_id: values.topic })),
14124
+ done: ([id]) => `Queued post import for prospect ${id}.`
14125
+ }],
14126
+ ["prospects sync", {
14127
+ usage: "audienti prospects sync <prsp_id> --social-cookie <id>",
14128
+ purpose: "Queue a message and email sync for one prospect through the selected LinkedIn account and other eligible sync accounts.",
14129
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/sync.json",
14130
+ options: { "social-cookie": { type: "string" } },
14131
+ required: ["social-cookie"],
14132
+ run: (client, accountId, [id], values) => client.syncProspect(accountId, id, compactObject({ social_cookie_id: values["social-cookie"] })),
14133
+ done: ([id]) => `Queued message sync for prospect ${id}.`
14134
+ }],
14135
+ ["prospects cancel-event", {
14136
+ usage: "audienti prospects cancel-event <prsp_id> --event <event_id>",
14137
+ purpose: "Cancel one scheduled event for a prospect.",
14138
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/cancel_scheduled_event.json",
14139
+ options: { event: { type: "string" } },
14140
+ required: ["event"],
14141
+ run: (client, accountId, [id], values) => client.cancelProspectScheduledEvent(accountId, id, { event_id: values.event }),
14142
+ done: ([id], payload) => `Canceled scheduled event ${display(payload?.event?.id)} for prospect ${id}.`
14143
+ }],
14144
+ ["prospects queue-draft", {
14145
+ usage: "audienti prospects queue-draft <prsp_id> [--context <json_object>] [--angle <n>]",
14146
+ purpose: "Write the queue draft for one prospect, like Write in the queue composer. Without --context the server uses the prospect's current queue action.",
14147
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/write.json",
14148
+ options: { context: { type: "string" }, angle: { type: "string" } },
14149
+ parse: (values) => ({ ...values, context: parseJsonObjectOption(values.context, "--context") }),
14150
+ run: (client, accountId, [id], values) => client.writeProspectQueueDraft(accountId, id, compactObject({ context: values.context, angle_index: values.angle })),
14151
+ done: ([id]) => `Wrote queue draft for prospect ${id}.`
14152
+ }],
14153
+ ["prospects rewrite", {
14154
+ usage: "audienti prospects rewrite <prsp_id> [--subject <text>] [--message <text>] [--angle <n>]",
14155
+ purpose: "Start a fresh queue draft for one prospect in the background.",
14156
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/rewrite.json",
14157
+ options: { subject: { type: "string" }, message: { type: "string" }, angle: { type: "string" } },
14158
+ run: (client, accountId, [id], values) => client.rewriteProspectQueueDraft(accountId, id, compactObject({ subject: values.subject, message: values.message, angle_index: values.angle })),
14159
+ done: ([id]) => `Started queue draft rewrite for prospect ${id}.`
14160
+ }],
14161
+ ["prospects engage", {
14162
+ usage: "audienti prospects engage <prsp_id> --type <action_type> [--message <text>] [--subject <text>] [--message-mode <mode>] [--social-cookie <id>] [--profile <id>] [--scheduled-at <iso8601>] [--post <post_id>] [--comment <comment_id>] [--reply-to <event_id>] [--request-event <event_id>] [--request-mode blank] [--queue-action] [--principal <account_user_id>]",
14163
+ purpose: "Run one manual engagement for a prospect with the same rules as the prospect composer. Use --post or --comment for post likes and comments, --reply-to for a message reply, and --queue-action to act like the queue composer (a connection request on a disconnected LinkedIn account is prepared, not sent).",
14164
+ api: "POST /api/v1/accounts/:account_id/prospects/:id/engage.json",
14165
+ options: {
14166
+ type: { type: "string" },
14167
+ message: { type: "string" },
14168
+ subject: { type: "string" },
14169
+ "message-mode": { type: "string" },
14170
+ "social-cookie": { type: "string" },
14171
+ profile: { type: "string" },
14172
+ "scheduled-at": { type: "string" },
14173
+ post: { type: "string" },
14174
+ comment: { type: "string" },
14175
+ "reply-to": { type: "string" },
14176
+ "request-event": { type: "string" },
14177
+ "request-mode": { type: "string" },
14178
+ "queue-action": { type: "boolean" },
14179
+ principal: { type: "string" }
14180
+ },
14181
+ required: ["type"],
14182
+ run: (client, accountId, [id], values) => client.engageProspect(accountId, id, compactObject({
14183
+ action_type: values.type,
14184
+ message: values.message,
14185
+ subject: values.subject,
14186
+ message_mode: values["message-mode"],
14187
+ engage_point_id: values["social-cookie"],
14188
+ profile_id: values.profile,
14189
+ scheduled_at: values["scheduled-at"],
14190
+ mention_id: values.post,
14191
+ comment_id: values.comment,
14192
+ message_id: values["reply-to"],
14193
+ request_event_id: values["request-event"],
14194
+ request_mode: values["request-mode"],
14195
+ queue_action: values["queue-action"] ? true : undefined,
14196
+ principal_account_user_id: values.principal
14197
+ })),
14198
+ done: (_positionals, payload) => display(payload?.message)
14199
+ }],
14200
+ ["prospects reject-selected", {
14201
+ usage: "audienti prospects reject-selected <prsp_id> [<prsp_id> ...]",
14202
+ minPositionals: 1,
14203
+ maxPositionals: Infinity,
14204
+ purpose: "Reject several prospects at once. Locked or unavailable prospects are reported as failed.",
14205
+ api: "POST /api/v1/accounts/:account_id/prospects/reject_selected.json",
14206
+ run: (client, accountId, ids) => client.rejectSelectedProspects(accountId, { prospect_ids: ids }),
14207
+ done: (_positionals, payload) => `Rejected ${payload?.rejected?.length ?? 0}; failed ${payload?.failed?.length ?? 0}.`
14208
+ }],
14209
+ ["prospects intake", {
14210
+ usage: "audienti prospects intake --url <linkedin_url> [--list <list_id>] [--assign <account_user_id>] [--signal <custom_signal_id>]",
14211
+ minPositionals: 0,
14212
+ maxPositionals: 0,
14213
+ purpose: "Add one LinkedIn person by URL, like Add prospect on the web.",
14214
+ api: "POST /api/v1/accounts/:account_id/prospects/intake.json",
14215
+ options: { url: { type: "string" }, list: { type: "string" }, assign: { type: "string" }, signal: { type: "string" } },
14216
+ required: ["url"],
14217
+ run: (client, accountId, _positionals, values) => client.intakeProspect(accountId, compactObject({
14218
+ url: values.url,
14219
+ list_id: values.list,
14220
+ assigned_to_account_user_id: values.assign,
14221
+ custom_signal_id: values.signal
14222
+ })),
14223
+ done: () => "Prospect intake queued."
14224
+ }],
14225
+ ["events retry", {
14226
+ usage: "audienti events retry <event_id>",
14227
+ purpose: "Retry one failed event, with the same eligibility rules as the web retry button.",
14228
+ api: "POST /api/v1/accounts/:account_id/events/:id/retry.json",
14229
+ run: (client, accountId, [id]) => client.retryEvent(accountId, id),
14230
+ done: ([id], payload) => `Retried event ${id} as ${display(payload?.event?.id)}.`
14231
+ }],
14232
+ ["profiles delete", {
14233
+ usage: "audienti profiles delete <profile_id>",
14234
+ purpose: "Remove one profile from its prospect. Backend admins only, like the web action.",
14235
+ api: "DELETE /api/v1/accounts/:account_id/profiles/:id.json",
14236
+ run: (client, accountId, [id]) => client.deleteProfile(accountId, id),
14237
+ done: ([id]) => `Removed profile ${id}.`
14238
+ }],
14239
+ ["companies stop-pursuing", {
14240
+ usage: "audienti companies stop-pursuing <company_profile_id>",
14241
+ purpose: "Stop pursuing one company, like Stop pursuing on the company page.",
14242
+ api: "POST /api/v1/accounts/:account_id/companies/:id/stop_pursuing.json",
14243
+ run: (client, accountId, [id]) => client.stopPursuingCompany(accountId, id),
14244
+ done: ([id]) => `Stopped pursuing company ${id}.`
14245
+ }],
14246
+ ["content defer", {
14247
+ usage: "audienti content defer <work_item_id> --for <1_day|1_week|1_month|3_months|6_months>",
14248
+ purpose: "Defer one content work item.",
14249
+ api: "POST /api/v1/accounts/:account_id/content_ops/work_items/:id/defer.json",
14250
+ options: { for: { type: "string" } },
14251
+ required: ["for"],
14252
+ run: (client, accountId, [id], values) => client.contentDefer(accountId, id, { delay_duration: values.for }),
14253
+ done: ([id]) => `Deferred work item ${id}.`
14254
+ }],
14255
+ ["inbox-ops update-filters", {
14256
+ usage: "audienti inbox-ops update-filters [--subscriptions <true|false>] [--automated-no-reply <true|false>] [--provider-promotions <true|false>] [--sender-allow <email>]... [--sender-filter <email>]... [--domain-allow <domain>]... [--domain-filter <domain>]...",
14257
+ minPositionals: 0,
14258
+ maxPositionals: 0,
14259
+ purpose: "Save the owner's Inbox Ops email filters. Rule flags replace the saved list for that rule.",
14260
+ api: "PATCH /api/v1/accounts/:account_id/inbox_ops/filters.json",
14261
+ options: {
14262
+ subscriptions: { type: "string" },
14263
+ "automated-no-reply": { type: "string" },
14264
+ "provider-promotions": { type: "string" },
14265
+ "sender-allow": { type: "string", multiple: true },
14266
+ "sender-filter": { type: "string", multiple: true },
14267
+ "domain-allow": { type: "string", multiple: true },
14268
+ "domain-filter": { type: "string", multiple: true }
14269
+ },
14270
+ run: (client, accountId, _positionals, values) => client.updateInboxOpsFilters(accountId, compactObject({
14271
+ subscriptions: values.subscriptions,
14272
+ automated_no_reply: values["automated-no-reply"],
14273
+ provider_promotions: values["provider-promotions"],
14274
+ sender_allow_rules: values["sender-allow"],
14275
+ sender_filter_rules: values["sender-filter"],
14276
+ domain_allow_rules: values["domain-allow"],
14277
+ domain_filter_rules: values["domain-filter"]
14278
+ })),
14279
+ done: () => "Saved Inbox Ops filters."
14280
+ }],
14281
+ ["inbox-ops reply", {
14282
+ usage: "audienti inbox-ops reply <row_id> --message <text> [--subject <text>]",
14283
+ purpose: "Queue a reply to one Inbox Ops row. Each run sends a new reply token.",
14284
+ api: "POST /api/v1/accounts/:account_id/inbox_ops/:row_id/reply.json",
14285
+ options: { message: { type: "string" }, subject: { type: "string" } },
14286
+ required: ["message"],
14287
+ run: (client, accountId, [rowId], values) => client.inboxOpsReply(accountId, rowId, compactObject({
14288
+ message: values.message,
14289
+ subject: values.subject,
14290
+ reply_token: randomUUID()
14291
+ })),
14292
+ done: ([rowId], payload) => `Reply ${display(payload?.status)} for ${rowId}.`
14293
+ }],
14294
+ ["inbox-ops draft-reply", {
14295
+ usage: "audienti inbox-ops draft-reply <row_id>",
14296
+ purpose: "Draft a reply for one Inbox Ops row without sending it.",
14297
+ api: "POST /api/v1/accounts/:account_id/inbox_ops/:row_id/reply/write.json",
14298
+ run: (client, accountId, [rowId]) => client.inboxOpsWriteReply(accountId, rowId),
14299
+ done: (_positionals, payload) => display(payload?.message ?? payload?.body)
14300
+ }],
14301
+ ["inbox-ops adopt", {
14302
+ usage: "audienti inbox-ops adopt <row_id> [--target-account <acct_id>] [--motion <motion_id>]",
14303
+ purpose: "Adopt the prospect behind one Inbox Ops row.",
14304
+ api: "POST /api/v1/accounts/:account_id/inbox_ops/:row_id/adopt.json",
14305
+ options: { "target-account": { type: "string" }, motion: { type: "string" } },
14306
+ run: (client, accountId, [rowId], values) => client.inboxOpsAdopt(accountId, rowId, compactObject({
14307
+ target_account_id: values["target-account"],
14308
+ motion_id: values.motion
14309
+ })),
14310
+ done: ([rowId]) => `Adopted ${rowId}.`
14311
+ }],
14312
+ ["network-ops adopt", {
14313
+ usage: "audienti network-ops adopt <row_id> --target-account <acct_id> --motion <motion_id>",
14314
+ purpose: "Accept one inbound connection request and adopt the person into a motion in an account owned by the request's LinkedIn account owner.",
14315
+ api: "POST /api/v1/accounts/:account_id/network_ops/:row_id/adopt.json",
14316
+ options: { "target-account": { type: "string" }, motion: { type: "string" } },
14317
+ required: ["target-account", "motion"],
14318
+ run: (client, accountId, [rowId], values) => client.networkOpsAction(accountId, rowId, "adopt", {
14319
+ target_account_id: values["target-account"],
14320
+ motion_id: values.motion
14321
+ }),
14322
+ done: ([rowId]) => `Adopted ${rowId}.`
14323
+ }],
14324
+ ["network-ops ignore", {
14325
+ usage: "audienti network-ops ignore <row_id>",
14326
+ purpose: "Ignore one inbound connection request without accepting or declining it.",
14327
+ api: "POST /api/v1/accounts/:account_id/network_ops/:row_id/ignore.json",
14328
+ run: (client, accountId, [rowId]) => client.networkOpsAction(accountId, rowId, "ignore"),
14329
+ done: ([rowId]) => `Ignored ${rowId}.`
14330
+ }],
14331
+ ["reconciliations add-to-motion", {
14332
+ usage: "audienti reconciliations add-to-motion <source_key> --motion <motion_id>",
14333
+ purpose: "Add one reconciliation candidate to a motion.",
14334
+ api: "POST /api/v1/accounts/:account_id/reconciliations/:id/add_to_motion.json",
14335
+ options: { motion: { type: "string" } },
14336
+ required: ["motion"],
14337
+ run: (client, accountId, [key], values) => client.reconciliationAddToMotion(accountId, key, { motion_id: values.motion }),
14338
+ done: ([key]) => `Added ${key} to the motion.`
14339
+ }],
14340
+ ["reconciliations ignore", {
14341
+ usage: "audienti reconciliations ignore <source_key>",
14342
+ purpose: "Ignore one reconciliation candidate.",
14343
+ api: "POST /api/v1/accounts/:account_id/reconciliations/:id/ignore.json",
14344
+ run: (client, accountId, [key]) => client.reconciliationIgnore(accountId, key),
14345
+ done: ([key]) => `Ignored ${key}.`
14346
+ }],
14347
+ // Motion list bulk actions, strategy retirement, launch checks and premise (#2301).
14348
+ ["motions bulk-add-tag", {
14349
+ usage: "audienti motions bulk-add-tag <motn_id> [<motn_id> ...] --tag <tag>",
14350
+ minPositionals: 1,
14351
+ maxPositionals: Infinity,
14352
+ purpose: "Add one tag to several motions, like Add tag on the motion list. Any motion outside the account fails the whole request.",
14353
+ api: "POST /api/v1/accounts/:account_id/motions/bulk_add_tag.json",
14354
+ options: { tag: { type: "string" } },
14355
+ required: ["tag"],
14356
+ run: (client, accountId, ids, values) => client.bulkAddMotionTag(accountId, { motion_ids: ids, tag: values.tag }),
14357
+ done: (_positionals, payload) => `Tagged ${payload?.length ?? 0} motions.`
14358
+ }],
14359
+ ["motions bulk-remove-tag", {
14360
+ usage: "audienti motions bulk-remove-tag <motn_id> [<motn_id> ...] --tag <tag>",
14361
+ minPositionals: 1,
14362
+ maxPositionals: Infinity,
14363
+ purpose: "Remove one tag from several motions, like Remove tag on the motion list.",
14364
+ api: "POST /api/v1/accounts/:account_id/motions/bulk_remove_tag.json",
14365
+ options: { tag: { type: "string" } },
14366
+ required: ["tag"],
14367
+ run: (client, accountId, ids, values) => client.bulkRemoveMotionTag(accountId, { motion_ids: ids, tag: values.tag }),
14368
+ done: (_positionals, payload) => `Removed the tag from ${payload?.length ?? 0} motions.`
14369
+ }],
14370
+ ["motions bulk-update-principal", {
14371
+ usage: "audienti motions bulk-update-principal <motn_id> [<motn_id> ...] --principal <account_user_id>",
14372
+ minPositionals: 1,
14373
+ maxPositionals: Infinity,
14374
+ purpose: "Give several motions the same owner, like Change owner on the motion list. The owner must be a user in this account.",
14375
+ api: "POST /api/v1/accounts/:account_id/motions/bulk_update_principal.json",
14376
+ options: { principal: { type: "string" } },
14377
+ required: ["principal"],
14378
+ run: (client, accountId, ids, values) => client.bulkUpdateMotionPrincipal(accountId, { motion_ids: ids, principal_account_user_id: values.principal }),
14379
+ done: (_positionals, payload) => `Assigned ${payload?.length ?? 0} motions.`
14380
+ }],
14381
+ ["motions bulk-update-status", {
14382
+ usage: "audienti motions bulk-update-status <motn_id> [<motn_id> ...] --status <preparing|active|closing|paused|archived>",
14383
+ minPositionals: 1,
14384
+ maxPositionals: Infinity,
14385
+ purpose: "Change the status of several motions with the same lifecycle rules as the motion list. Motions the rules block keep their status and are listed with the reason.",
14386
+ api: "POST /api/v1/accounts/:account_id/motions/bulk_update_status.json",
14387
+ options: { status: { type: "string" } },
14388
+ required: ["status"],
14389
+ run: (client, accountId, ids, values) => client.bulkUpdateMotionStatus(accountId, { motion_ids: ids, status: values.status }),
14390
+ done: (_positionals, payload) => [
14391
+ `Set ${payload?.applied_count ?? 0} motions to ${display(payload?.status)}; ${payload?.blocked_count ?? 0} blocked.`,
14392
+ ...(payload?.blocked || []).map((row) => ` ${row.motion_id}: ${display(row.reason)}`)
14393
+ ].join("\n")
14394
+ }],
14395
+ ["motions retire-strategy", {
14396
+ usage: "audienti motions retire-strategy <motn_id> --strategy <strategy_id>",
14397
+ purpose: "Retire one strategy of an inbound motion. Retirement is permanent; the slot refills from the backlog on the next planning pass.",
14398
+ api: "POST /api/v1/accounts/:account_id/motions/:id/strategies/:motion_search_scope_id/retire.json",
14399
+ options: { strategy: { type: "string" } },
14400
+ required: ["strategy"],
14401
+ run: (client, accountId, [id], values) => client.retireMotionStrategy(accountId, id, values.strategy),
14402
+ done: (_positionals, payload) => (payload?.already_retired
14403
+ ? `Strategy ${display(payload?.strategy?.id)} is already retired.`
14404
+ : `Retired strategy ${display(payload?.strategy?.id)}.`)
14405
+ }],
14406
+ ["motions refresh-launch-check", {
14407
+ usage: "audienti motions refresh-launch-check <motn_id>",
14408
+ purpose: "Recheck whether one motion can launch discovery, without starting discovery.",
14409
+ api: "POST /api/v1/accounts/:account_id/motions/:id/refresh_launch_check.json",
14410
+ run: (client, accountId, [id]) => client.refreshMotionLaunchCheck(accountId, id),
14411
+ done: (_positionals, payload) => `${display(payload?.name)}: demand ${display(payload?.demand_status)}, producer ${display(payload?.producer_status)}, reason ${display(payload?.reason)}.`
14412
+ }],
14413
+ ["motions refresh-launch-checks", {
14414
+ usage: "audienti motions refresh-launch-checks",
14415
+ minPositionals: 0,
14416
+ maxPositionals: 0,
14417
+ purpose: "Recheck launch readiness for every motion in the account, without starting discovery.",
14418
+ api: "POST /api/v1/accounts/:account_id/motions/refresh_launch_checks.json",
14419
+ run: (client, accountId) => client.refreshMotionLaunchChecks(accountId),
14420
+ done: (_positionals, payload) => `Launch checks refreshed: ${display(payload?.summary)}.`
14421
+ }],
14422
+ ["motions update-premise", {
14423
+ usage: "audienti motions update-premise <motn_id> --premise <text>",
14424
+ purpose: "Save a motion's premise and rebuild its ICP targeting from it, like Update premise in the Operator.",
14425
+ api: "PATCH /api/v1/accounts/:account_id/motions/:id/update_premise.json",
14426
+ options: { premise: { type: "string" } },
14427
+ required: ["premise"],
14428
+ run: (client, accountId, [id], values) => client.updateMotionPremise(accountId, id, { motion: { premise: values.premise } }),
14429
+ done: ([id]) => `Updated the premise for ${id}.`
14430
+ }]
14431
+ ]);
14432
+
14433
+ for (const [topic, spec] of PARITY_ACTION_COMMANDS) {
14434
+ if (HELP_TOPICS.has(topic)) throw new Error(`duplicate help topic ${topic}`);
14435
+ HELP_TOPICS.set(topic, [
14436
+ "Usage:",
14437
+ ` ${spec.usage} ${ACCOUNT_OPTION_USAGE}`,
14438
+ "",
14439
+ "Status: implemented",
14440
+ "",
14441
+ "Purpose:",
14442
+ ` ${spec.purpose}`,
14443
+ "",
14444
+ "API:",
14445
+ ` ${spec.api}`
14446
+ ].join("\n"));
14447
+ }
14448
+
14449
+ // List the single-purpose actions under their group help (`audienti help prospects`).
14450
+ const parityActionUsagesByGroup = new Map();
14451
+ for (const [topic, spec] of PARITY_ACTION_COMMANDS) {
14452
+ const group = topic.split(" ")[0];
14453
+ if (!parityActionUsagesByGroup.has(group)) parityActionUsagesByGroup.set(group, []);
14454
+ parityActionUsagesByGroup.get(group).push(` ${spec.usage} ${ACCOUNT_OPTION_USAGE}`);
14455
+ }
14456
+ for (const [group, usages] of parityActionUsagesByGroup) {
14457
+ const section = ["Actions:", ...usages].join("\n");
14458
+ const existing = HELP_TOPICS.get(group);
14459
+ HELP_TOPICS.set(group, existing ? `${existing}\n\n${section}` : section);
14460
+ }
14461
+
14462
+ function parseJsonObjectOption(raw, flag) {
14463
+ if (raw === undefined) return undefined;
14464
+ let parsed;
14465
+ try {
14466
+ parsed = JSON.parse(raw);
14467
+ } catch {
14468
+ throw new CommandError(`${flag} must be a JSON object, for example '{"selected_action":{"key":"send_email"}}'.`);
14469
+ }
14470
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
14471
+ throw new CommandError(`${flag} must be a JSON object, for example '{"selected_action":{"key":"send_email"}}'.`);
14472
+ }
14473
+ return parsed;
14474
+ }
14475
+
14476
+ async function runParityActionCommand(topic, args, context, { accountOverride } = {}) {
14477
+ const spec = PARITY_ACTION_COMMANDS.get(topic);
14478
+ const usageLine = `Usage: ${spec.usage} ${ACCOUNT_OPTION_USAGE}`;
14479
+ const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), ...(spec.options || {}) });
14480
+ const minPositionals = spec.minPositionals ?? 1;
14481
+ const maxPositionals = spec.maxPositionals ?? 1;
14482
+ if (positionals.length < minPositionals || positionals.length > maxPositionals) throw new CommandError(usageLine);
14483
+ if ((spec.required || []).some((key) => !values[key])) throw new CommandError(usageLine);
14484
+
14485
+ const parsedValues = spec.parse ? spec.parse(values, usageLine) : values;
14486
+
14487
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
14488
+ const payload = await spec.run(client, accountId, positionals, parsedValues);
14489
+ if (values.json) return writeJson(context.stdout, payload);
14490
+
14491
+ writeLine(context.stdout, spec.done(positionals, payload));
14492
+ }