@audienti/cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to the Audienti CLI are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.8] - 2026-07-14
8
+
9
+ ### Added
10
+
11
+ - Add `audienti motions clone <motn_id> --name <text>` to clone a motion/play config through the API without copying people.
12
+ - Add `audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]` to transfer prospects between motions/plays.
13
+
14
+ ## [0.1.7] - 2026-07-12
15
+
16
+ ### Fixed
17
+
18
+ - Show writer test-run target-step error status and warnings when a draft fails instead of rendering an empty drafted-copy block.
19
+
7
20
  ## [0.1.6] - 2026-07-12
8
21
 
9
22
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api-client.js CHANGED
@@ -130,6 +130,20 @@ export class AudientiClient {
130
130
  });
131
131
  }
132
132
 
133
+ cloneMotion(accountId, motionId, body) {
134
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "clone"]), {
135
+ method: "POST",
136
+ body
137
+ });
138
+ }
139
+
140
+ moveMotionProspects(accountId, motionId, body) {
141
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "move_prospects"]), {
142
+ method: "POST",
143
+ body
144
+ });
145
+ }
146
+
133
147
  motionStatus(accountId, motionId) {
134
148
  return this.requestJson(accountPath(accountId, ["motions", motionId, "status"]));
135
149
  }
package/src/cli.js CHANGED
@@ -31,6 +31,8 @@ const PROSPECTS_ADD_PROFILE_USAGE = "Usage: audienti prospects add-profile <prsp
31
31
  const PROSPECTS_REPORT_BAD_PROFILE_USAGE = "Usage: audienti prospects report-bad-profile <prsp_id> <prof_id|citation_id> [--json] [--account <acct_id>]";
32
32
  const WRITER_TEST_RUN_USAGE = "Usage: audienti writer test-run <prsp_id> [--json] [--mode <report|plan|step>] [--branch <both|no-accept|accepted>] [--step <step_key|row_number>] [--no-cache] [--clear-cache] [--account <acct_id>]";
33
33
  const MOTIONS_ANALYTICS_USAGE = "Usage: audienti motions analytics <motn_id> [--window 30d] [--json] [--account <acct_id>]";
34
+ const MOTIONS_CLONE_USAGE = "Usage: audienti motions clone <motn_id> --name <text> [--json] [--account <acct_id>]";
35
+ 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>]";
34
36
  const ANALYTICS_PROSPECTS_USAGE = "Usage: audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
35
37
  const ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE = "Usage: audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
36
38
  const ANALYTICS_USERS_USAGE = "Usage: audienti analytics users [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--provenance <source>] [--platform <linkedin|email|gmail>] [--json] [--account <acct_id>]";
@@ -135,6 +137,8 @@ async function dispatch(argv, context) {
135
137
  if (normalizedResource === "motions" && action === "prospects") return motionsProspects(rest, context, { accountOverride });
136
138
  if (normalizedResource === "motions" && action === "add-prospects") return motionsAddProspects(rest, context, { accountOverride });
137
139
  if (normalizedResource === "motions" && action === "create") return motionsCreate(rest, context, { accountOverride });
140
+ if (normalizedResource === "motions" && action === "clone") return motionsClone(rest, context, { accountOverride });
141
+ if (normalizedResource === "motions" && action === "move-prospects") return motionsMoveProspects(rest, context, { accountOverride });
138
142
  if (normalizedResource === "prospects" && action === "list") return prospectsList(rest, context, { accountOverride });
139
143
  if (normalizedResource === "prospects" && action === "show") return prospectsShow(rest, context, { accountOverride });
140
144
  if (normalizedResource === "prospects" && action === "timeline") return prospectsTimeline(rest, context, { accountOverride });
@@ -782,6 +786,52 @@ async function motionsCreate(args, context, { accountOverride } = {}) {
782
786
  renderMotion(created, context);
783
787
  }
784
788
 
789
+ async function motionsClone(args, context, { accountOverride } = {}) {
790
+ const { values, positionals } = parseCommandArgs(args, {
791
+ ...jsonOptions(),
792
+ name: { type: "string" }
793
+ });
794
+ if (positionals.length !== 1 || !values.name) {
795
+ throw new CommandError(MOTIONS_CLONE_USAGE);
796
+ }
797
+
798
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
799
+ const cloned = await client.cloneMotion(accountId, positionals[0], {
800
+ motion: {
801
+ name: values.name
802
+ }
803
+ });
804
+ if (values.json) return writeJson(context.stdout, cloned);
805
+
806
+ writeLine(context.stdout, `Cloned motion ${display(positionals[0])} as ${display(cloned?.name)} (${display(cloned?.prefix_id)}).`);
807
+ renderMotion(cloned, context);
808
+ }
809
+
810
+ async function motionsMoveProspects(args, context, { accountOverride } = {}) {
811
+ const { values, positionals } = parseCommandArgs(args, {
812
+ ...jsonOptions(),
813
+ target: { type: "string" }
814
+ });
815
+ if (positionals.length < 2 || !values.target) {
816
+ throw new CommandError(MOTIONS_MOVE_PROSPECTS_USAGE);
817
+ }
818
+
819
+ const [sourceMotionId, ...prospectIds] = positionals;
820
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
821
+ const payload = await client.moveMotionProspects(accountId, sourceMotionId, {
822
+ target_motion_id: values.target,
823
+ prospect_ids: prospectIds
824
+ });
825
+ if (values.json) return writeJson(context.stdout, payload);
826
+
827
+ const moved = Number(payload?.moved || 0);
828
+ const failed = Array.isArray(payload?.failed) ? payload.failed.length : 0;
829
+ writeLine(context.stdout, `Moved ${moved} prospects from ${display(sourceMotionId)} to ${display(values.target)}.`);
830
+ if (failed > 0) {
831
+ writeLine(context.stdout, `${failed} prospects failed.`);
832
+ }
833
+ }
834
+
785
835
  async function prospectsList(args, context, { accountOverride } = {}) {
786
836
  const { values, positionals } = parseCommandArgs(args, {
787
837
  ...jsonOptions(),
@@ -1113,7 +1163,7 @@ function writerCachedDraftsForRequest(cache, branchFilter) {
1113
1163
  const branchKeys = writerRequestedBranchKeys(branchFilter);
1114
1164
  return Object.values(cache.entries || {})
1115
1165
  .filter((entry) => branchKeys.includes(entry.branch))
1116
- .filter((entry) => entry.key && (entry.body || entry.text || entry.subject))
1166
+ .filter((entry) => entry.key && (entry.body || entry.text || entry.subject || writerCacheQualityFailure(entry)))
1117
1167
  .map((entry) => compactObject({
1118
1168
  branch: entry.branch,
1119
1169
  key: entry.key,
@@ -1125,6 +1175,9 @@ function writerCachedDraftsForRequest(cache, branchFilter) {
1125
1175
  body: entry.body,
1126
1176
  text: entry.text,
1127
1177
  status: entry.status,
1178
+ quality_codes: entry.quality_codes,
1179
+ blank_reason: entry.blank_reason,
1180
+ writer_path: entry.writer_path,
1128
1181
  generated_at: entry.generated_at,
1129
1182
  writer_engine: entry.writer_engine,
1130
1183
  target: entry.target,
@@ -1187,7 +1240,7 @@ async function persistWriterDraftsFromPayload(context, { cache, payload }) {
1187
1240
  function writerCacheEntryFromStep(step, { branchKey, generatedAt }) {
1188
1241
  if (step?.kind !== "message") return null;
1189
1242
  if (!step.key) return null;
1190
- if (!(step.body || step.text || step.subject)) return null;
1243
+ if (!(step.body || step.text || step.subject || writerCacheQualityFailure(step))) return null;
1191
1244
  if (step.status === "planned" || step.status === "unavailable" || step.status === "error") return null;
1192
1245
 
1193
1246
  return compactObject({
@@ -1201,8 +1254,11 @@ function writerCacheEntryFromStep(step, { branchKey, generatedAt }) {
1201
1254
  body: step.body,
1202
1255
  text: step.text,
1203
1256
  status: step.status,
1257
+ quality_codes: Array.isArray(step.quality_codes) ? step.quality_codes.filter(Boolean) : undefined,
1258
+ blank_reason: step.blank_reason,
1259
+ writer_path: step.writer_path,
1204
1260
  generated_at: step.generated_at || generatedAt || new Date().toISOString(),
1205
- writer_engine: step?.metadata?.writer_engine,
1261
+ writer_engine: step.writer_engine || step?.metadata?.writer_engine,
1206
1262
  target: step.target,
1207
1263
  metadata: step.metadata
1208
1264
  });
@@ -1212,6 +1268,10 @@ function writerCacheEntryKey(entry) {
1212
1268
  return `${entry.branch}:${entry.key}`;
1213
1269
  }
1214
1270
 
1271
+ function writerCacheQualityFailure(entry) {
1272
+ return String(entry?.status || "") === "quality_failure";
1273
+ }
1274
+
1215
1275
  async function prospectsImport(args, context, { accountOverride } = {}) {
1216
1276
  const { values, positionals } = parseCommandArgs(args, {
1217
1277
  ...jsonOptions(),
@@ -2343,7 +2403,7 @@ function renderWriterStepRow(step, index, context) {
2343
2403
  fixedWidth(compactKindLabel(step.kind), 4),
2344
2404
  fixedWidth(writerStepAction(step), 42),
2345
2405
  fixedWidth(compactChannelLabel(step.channel), 7),
2346
- fixedWidth(display(step.status), 9)
2406
+ fixedWidth(compactWriterStatusLabel(step.status), 9)
2347
2407
  ].join(" ");
2348
2408
  writeLine(context.stdout, row);
2349
2409
  }
@@ -2359,8 +2419,21 @@ function renderWriterTargetDraft(branch, context) {
2359
2419
  writeLine(context.stdout, `Drafted copy: ${display(draftedStep.stage)}${branch.resolved_target_step ? ` (${branch.resolved_target_step})` : ""}`);
2360
2420
  const targetUrl = writerDraftTargetUrl(draftedStep);
2361
2421
  if (targetUrl) writeLine(context.stdout, `Replying to: ${targetUrl}`);
2422
+ if (draftedStep.status && draftedStep.status !== "success") writeLine(context.stdout, `Status: ${draftedStep.status}`);
2423
+ if (String(draftedStep.status || "") === "quality_failure") {
2424
+ const qualityCodes = Array.isArray(draftedStep.quality_codes) ? draftedStep.quality_codes.filter(Boolean) : [];
2425
+ if (qualityCodes.length) writeLine(context.stdout, `Quality failure: ${qualityCodes.join(", ")}`);
2426
+ if (draftedStep.blank_reason) writeLine(context.stdout, `Blank reason: ${draftedStep.blank_reason}`);
2427
+ if (draftedStep.writer_path) writeLine(context.stdout, `Writer path: ${draftedStep.writer_path}`);
2428
+ if (draftedStep.writer_engine) writeLine(context.stdout, `Writer engine: ${draftedStep.writer_engine}`);
2429
+ }
2430
+ for (const warning of Array.isArray(draftedStep.warnings) ? draftedStep.warnings : []) {
2431
+ if (warning) writeLine(context.stdout, `Warning: ${warning}`);
2432
+ }
2433
+ if (draftedStep.missing_reason) writeLine(context.stdout, `Missing reason: ${draftedStep.missing_reason}`);
2362
2434
  if (draftedStep.subject) writeLine(context.stdout, `Subject: ${draftedStep.subject}`);
2363
- writeLine(context.stdout, display(draftedStep.body || draftedStep.text || draftedStep.empty_body_reason || ""));
2435
+ const body = draftedStep.body || draftedStep.text || draftedStep.empty_body_reason;
2436
+ writeLine(context.stdout, display(body || "No draft body returned."));
2364
2437
  }
2365
2438
 
2366
2439
  function writerDraftTargetUrl(step) {
@@ -2414,6 +2487,12 @@ function compactChannelLabel(channel) {
2414
2487
  return value;
2415
2488
  }
2416
2489
 
2490
+ function compactWriterStatusLabel(status) {
2491
+ const value = String(display(status));
2492
+ if (value === "quality_failure") return "quality";
2493
+ return value;
2494
+ }
2495
+
2417
2496
  function fixedWidth(value, width, options = {}) {
2418
2497
  const text = truncateCliText(value, width);
2419
2498
  return options.align === "right" ? text.padStart(width) : text.padEnd(width);
@@ -3241,6 +3320,8 @@ const HELP_TOPICS = new Map([
3241
3320
  " audienti motions analytics <motn_id>",
3242
3321
  " audienti motions prospects <motn_id>",
3243
3322
  " audienti motions create --payload <file.json>",
3323
+ " audienti motions clone <motn_id> --name <text>",
3324
+ " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]",
3244
3325
  " Tip: `plays` is accepted anywhere `motions` is accepted.",
3245
3326
  "",
3246
3327
  " Prospects",
@@ -3813,8 +3894,10 @@ const HELP_TOPICS = new Map([
3813
3894
  " audienti motions prospects <motn_id> [--json]",
3814
3895
  " audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
3815
3896
  " audienti motions create --payload <file.json> [--json]",
3897
+ " audienti motions clone <motn_id> --name <text> [--json]",
3898
+ " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...] [--json]",
3816
3899
  "",
3817
- "Status: read, create, status, and prospect attachment commands implemented",
3900
+ "Status: read, create, clone, status, and prospect attachment commands implemented",
3818
3901
  "",
3819
3902
  "CLI synonym:",
3820
3903
  " `plays` is accepted anywhere `motions` is accepted",
@@ -3988,6 +4071,62 @@ const HELP_TOPICS = new Map([
3988
4071
  " Use `audienti offers list`, `audienti icps list`, and `audienti users list` to resolve valid ids before calling this command."
3989
4072
  ].join("\n")],
3990
4073
 
4074
+ ["motions clone", [
4075
+ "Usage:",
4076
+ ` ${MOTIONS_CLONE_USAGE.slice("Usage: ".length)}`,
4077
+ "",
4078
+ "Status: implemented",
4079
+ "",
4080
+ "Purpose:",
4081
+ " Clone one motion or play's configuration under a new name without copying its people.",
4082
+ "",
4083
+ "Input shape:",
4084
+ " motn_id: motn_ prefix id",
4085
+ " name: new motion name",
4086
+ "",
4087
+ "Behavior:",
4088
+ " Copies the motion kind, offer, ICP, principal, premise, approach, targeting profile, suppression policy, secondary roles, and active signal rows.",
4089
+ " The clone starts as draft with a new empty backing list.",
4090
+ "",
4091
+ "API:",
4092
+ " POST /api/v1/accounts/:account_id/motions/:id/clone.json",
4093
+ "",
4094
+ "JSON body:",
4095
+ " {",
4096
+ " \"motion\": {",
4097
+ " \"name\": \"Wine Campaign Restaurant Operators\"",
4098
+ " }",
4099
+ " }"
4100
+ ].join("\n")],
4101
+
4102
+ ["motions move-prospects", [
4103
+ "Usage:",
4104
+ ` ${MOTIONS_MOVE_PROSPECTS_USAGE.slice("Usage: ".length)}`,
4105
+ "",
4106
+ "Status: implemented",
4107
+ "",
4108
+ "Purpose:",
4109
+ " Move prospects out of one motion or play and into another motion or play.",
4110
+ "",
4111
+ "Input shape:",
4112
+ " source_motn_id: source motn_ prefix id",
4113
+ " target_motn_id: target motn_ prefix id",
4114
+ " prsp_id: one or more prospect prefix ids",
4115
+ "",
4116
+ "Behavior:",
4117
+ " Move removes each selected prospect from the source motion and source backing list, then adds it to the target motion and target backing list.",
4118
+ " Copy is intentionally not exposed until multi-motion membership exists.",
4119
+ "",
4120
+ "API:",
4121
+ " POST /api/v1/accounts/:account_id/motions/:id/move_prospects.json",
4122
+ "",
4123
+ "JSON body:",
4124
+ " {",
4125
+ " \"target_motion_id\": \"motn_target\",",
4126
+ " \"prospect_ids\": [\"prsp_one\", \"prsp_two\"]",
4127
+ " }"
4128
+ ].join("\n")],
4129
+
3991
4130
  ["prospects", [
3992
4131
  "Usage:",
3993
4132
  " audienti prospects list [--json] [filters]",
@@ -4733,6 +4872,8 @@ const HELP_TOPICS = new Map([
4733
4872
  "",
4734
4873
  "2. Create a motion or play",
4735
4874
  " audienti motions create --payload <file.json>",
4875
+ " audienti motions clone <motn_id> --name \"New subset motion\"",
4876
+ " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]",
4736
4877
  " audienti motions status <motn_id>",
4737
4878
  "",
4738
4879
  "3. Add a new prospect from LinkedIn and poll enrichment",