@audienti/cli 0.1.55 → 0.1.56

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,13 @@ All notable changes to the Audienti CLI are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.56] - 2026-09-05
8
+
9
+ ### Added
10
+
11
+ - Configure Motion Approach-guided adaptive post-accept planning with Approach alone; explicit clearing restores the existing sequence, without a separate mode selector.
12
+ - Answer current planner or writer questions with `operator answer`, exact-row refetching and stale-context protection; show prompts and candidate options in queue and next output.
13
+
7
14
  ## [0.1.55] - 2026-09-05
8
15
 
9
16
  ### Changed
package/README.md CHANGED
@@ -7,6 +7,10 @@ create and manage plays, import prospects, build lists, manage task reminders,
7
7
  configure list routing rules and account-user automation safety, and work
8
8
  supported operator flows.
9
9
 
10
+ The Motion Approach guides post-accept planning and writing. Set it with `audienti motions update motn_123 --approach "Test the premise before asking for a meeting"` or the `approach` field in a create/update payload. A nonblank Approach automatically chooses adaptive planning; blank, nil, or whitespace uses the existing sequence. `--approach ""` explicitly clears it and restores the existing sequence. There is no separate planning-mode selector. Existing sending controls still apply.
11
+
12
+ `audienti operator queue` and `operator next` show planner and writer questions with candidate options. Answer one with `audienti operator answer <row_id> --choice <id>` or `--answer "Your guidance"`. The command fetches that exact row even when it is not the next move, then submits its current fingerprints. Stale or double answers return 409; an accepted answer returns 202 planning. Answers guide only the current decision. Planning and preview never authorize sending.
13
+
10
14
  ## Install
11
15
 
12
16
  Requires Node.js 20 or newer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,10 @@ the production contract. The packaged `audienti-mcp` command is only a local
10
10
  stdio bridge for MCP hosts. Do not build a parallel wrapper or call
11
11
  undocumented API endpoints.
12
12
 
13
+ Set `approach` in a Motion create/update payload, or use `motions update <id> --approach "..."`. A nonblank Approach automatically guides adaptive planning and writing; blank, nil, or whitespace keeps the existing sequence. The simple `--approach ""` flag explicitly clears the guidance and restores that sequence. There is no separate planning-mode selector, and existing sending controls still apply.
14
+
15
+ When Operator exposes `answer_planner_question`, read its prompt and candidate options with `operator queue` or `operator next`. Use `operator answer <row_id> --choice <id>` or `--answer "..."`, exactly one input. This command refetches the exact row and submits the current row and decision fingerprints. Do not substitute a generic outcome, a reusable steer note, or a guessed send. A 409 means refresh the question; 202 means the answer was recorded and planning continues. Answers apply only to that decision.
16
+
13
17
  ## Setup
14
18
 
15
19
  1. Verify the command is installed:
package/src/api-client.js CHANGED
@@ -686,6 +686,14 @@ export class AudientiClient {
686
686
  return this.requestJson(accountPath(accountId, ["operator", "next"], query));
687
687
  }
688
688
 
689
+ operatorRow(accountId, rowId, query = {}) {
690
+ return this.requestJson(accountPath(accountId, ["operator", "row"], { ...query, row_id: rowId }));
691
+ }
692
+
693
+ operatorAnswer(accountId, body) {
694
+ return this.requestJson(accountPath(accountId, ["operator", "answer"]), { method: "POST", body });
695
+ }
696
+
689
697
  requeueOperatorFailedDrafts(accountId, body = {}) {
690
698
  return this.requestJson(accountPath(accountId, ["operator", "failed_drafts", "requeue"]), {
691
699
  method: "POST",
package/src/cli.js CHANGED
@@ -91,7 +91,7 @@ const OFFERS_DELETE_USAGE = "Usage: audienti offers delete <offr_id> --confirm <
91
91
  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>]";
92
92
  const WRITER_TEST_RUN_SHOW_USAGE = "Usage: audienti writer test-run show <prsp_id> <rprt_id> [--json] [--account <acct_id>]";
93
93
  const MOTIONS_ANALYTICS_USAGE = "Usage: audienti motions analytics <motn_id> [--window 30d] [--json] [--account <acct_id>]";
94
- const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> ([--status <draft|preparing|active|closing|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>]";
94
+ const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> ([--status <draft|preparing|active|closing|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>] [--approach <text>] | --payload <file.json>) [--json] [--account <acct_id>]";
95
95
  const CONTENT_PROGRAMS_USAGE = "Usage: audienti content programs [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
96
96
  const CONTENT_PLAN_USAGE = "Usage: audienti content plan <cprg_id> [--week <n>] [--due] [--json] [--account <acct_id>]";
97
97
  const CONTENT_SHOW_USAGE = "Usage: audienti content show <cpwi_id> [--json] [--account <acct_id>]";
@@ -336,6 +336,7 @@ async function dispatch(argv, context) {
336
336
  if (normalizedResource === "operator" && action === "queue") return operatorQueue(rest, context, { accountOverride });
337
337
  if (normalizedResource === "operator" && action === "next") return operatorNext(rest, context, { accountOverride });
338
338
  if (normalizedResource === "operator" && action === "outcome") return operatorOutcome(rest, context, { accountOverride });
339
+ if (normalizedResource === "operator" && action === "answer") return operatorAnswer(rest, context, { accountOverride });
339
340
  if (normalizedResource === "inbox-ops" && action === "queue") return inboxOpsQueue(rest, context, { accountOverride });
340
341
  if (normalizedResource === "inbox-ops" && action === "filters") return inboxOpsFilters(rest, context, { accountOverride });
341
342
  if (normalizedResource === "inbox-ops" && action === "rule") return inboxOpsRule(rest, context, { accountOverride });
@@ -1923,6 +1924,7 @@ async function motionsCreate(args, context, { accountOverride } = {}) {
1923
1924
 
1924
1925
  function normalizeMotionCreatePayload(payload) {
1925
1926
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload;
1927
+ rejectMotionPlanningMode(payload);
1926
1928
  validateMotionSignalRows(payload);
1927
1929
  if (String(payload.kind || "").trim().toLowerCase() !== "inbound") return payload;
1928
1930
  if (Object.prototype.hasOwnProperty.call(payload, "inbound_channels")) return payload;
@@ -2007,6 +2009,7 @@ async function motionsUpdate(args, context, { accountOverride } = {}) {
2007
2009
  ...jsonOptions(),
2008
2010
  payload: { type: "string" },
2009
2011
  status: { type: "string" },
2012
+ approach: { type: "string" },
2010
2013
  tags: { type: "string" },
2011
2014
  "own-post-engagement": { type: "string" },
2012
2015
  "start-date": { type: "string" },
@@ -2033,6 +2036,7 @@ async function motionUpdatePayload(values) {
2033
2036
  }
2034
2037
 
2035
2038
  const payload = await readJsonPayload(values.payload);
2039
+ rejectMotionPlanningMode(payload);
2036
2040
  validateMotionSignalRows(payload);
2037
2041
  return payload;
2038
2042
  }
@@ -2043,6 +2047,7 @@ async function motionUpdatePayload(values) {
2043
2047
  own_post_engagement: values["own-post-engagement"] !== undefined ? parseBooleanString(values["own-post-engagement"], "--own-post-engagement") : undefined
2044
2048
  });
2045
2049
  const nullableSettings = {
2050
+ approach: values.approach,
2046
2051
  starts_on: normalizeMotionDate(values["start-date"], "--start-date"),
2047
2052
  ends_on: normalizeMotionDate(values["end-date"], "--end-date"),
2048
2053
  maximum_company_count: normalizeMotionMaximumCompanyCount(values["maximum-company-count"])
@@ -2056,6 +2061,7 @@ async function motionUpdatePayload(values) {
2056
2061
 
2057
2062
  function motionSimpleFieldsPresent(values) {
2058
2063
  return Boolean(values.status) ||
2064
+ values.approach !== undefined ||
2059
2065
  values.tags !== undefined ||
2060
2066
  values["own-post-engagement"] !== undefined ||
2061
2067
  values["start-date"] !== undefined ||
@@ -2063,6 +2069,12 @@ function motionSimpleFieldsPresent(values) {
2063
2069
  values["maximum-company-count"] !== undefined;
2064
2070
  }
2065
2071
 
2072
+ function rejectMotionPlanningMode(payload) {
2073
+ if (payload && Object.prototype.hasOwnProperty.call(payload, "post_accept_planning_mode")) {
2074
+ throw new CommandError("post_accept_planning_mode is read-only. Set a nonblank Approach for adaptive planning, or clear Approach to use the existing sequence.");
2075
+ }
2076
+ }
2077
+
2066
2078
  function normalizeMotionDate(value, flagName) {
2067
2079
  if (value === undefined) return undefined;
2068
2080
 
@@ -3412,6 +3424,39 @@ async function operatorOutcome(args, context, { accountOverride } = {}) {
3412
3424
  renderOperatorOutcome(response, context);
3413
3425
  }
3414
3426
 
3427
+ async function operatorAnswer(args, context, { accountOverride } = {}) {
3428
+ const { values, positionals } = parseCommandArgs(args, {
3429
+ ...jsonOptions(),
3430
+ choice: { type: "string" },
3431
+ principal: { type: "string" },
3432
+ answer: { type: "string" }
3433
+ });
3434
+ if (positionals.length !== 1 || (values.choice !== undefined) === (values.answer !== undefined)) {
3435
+ throw new CommandError("Usage: audienti operator answer <row_id> (--choice <id> | --answer <text>) [--json] [--account <acct_id>]");
3436
+ }
3437
+ if (!String(values.choice ?? values.answer).trim()) throw new CommandError("The choice or answer must not be blank.");
3438
+
3439
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
3440
+ const filters = compactObject({ principal_account_user_id: values.principal });
3441
+ const payload = await client.operatorRow(accountId, positionals[0], filters);
3442
+ const row = payload?.row;
3443
+ const action = row?.next_action;
3444
+ if (String(row?.id) !== positionals[0] || action?.type !== "answer_planner_question" ||
3445
+ !row.fingerprint || !action.decision_id || !action.context_fingerprint) {
3446
+ throw new CommandError("This row has no current planner question. Refresh the Operator queue.");
3447
+ }
3448
+ const response = await client.operatorAnswer(accountId, {
3449
+ ...filters,
3450
+ row_id: row.id,
3451
+ fingerprint: row.fingerprint,
3452
+ decision_id: action.decision_id,
3453
+ context_fingerprint: action.context_fingerprint,
3454
+ ...(values.choice !== undefined ? { choice_id: values.choice } : { answer: values.answer })
3455
+ });
3456
+ if (values.json) return writeJson(context.stdout, response);
3457
+ writeLine(context.stdout, "Answer recorded. Planning the next step.");
3458
+ }
3459
+
3415
3460
  async function analyticsProspects(args, context, { accountOverride } = {}) {
3416
3461
  if (args[0] === "cohort-analysis") {
3417
3462
  return analyticsProspectsCohortAnalysis(args.slice(1), context, { accountOverride });
@@ -5321,6 +5366,10 @@ function renderMotion(motion, context) {
5321
5366
  writeLine(context.stdout, `Motion: ${display(motion?.name)} (${display(motion?.prefix_id)})`);
5322
5367
  writeLine(context.stdout, `Status: ${display(motion?.status)}`);
5323
5368
  writeLine(context.stdout, `Kind: ${display(motion?.kind)}`);
5369
+ writeLine(context.stdout, `Approach: ${display(motion?.approach, "not set")}`);
5370
+ if (typeof motion?.post_accept_actions_enabled === "boolean") {
5371
+ writeLine(context.stdout, `Post-accept actions: ${motion.post_accept_actions_enabled ? "enabled" : "disabled"}`);
5372
+ }
5324
5373
  writeLine(context.stdout, `Start date: ${display(motion?.starts_on, "not set")}`);
5325
5374
  writeLine(context.stdout, `End date: ${display(motion?.ends_on, "not set")}`);
5326
5375
  writeLine(context.stdout, `Maximum companies: ${display(motion?.maximum_company_count, "not set")}`);
@@ -5973,6 +6022,10 @@ function renderSequenceStep(step, index, context) {
5973
6022
  if (step.body) writeLine(context.stdout, ` Body: ${step.body}`);
5974
6023
  if (step.empty_body_reason) writeLine(context.stdout, ` Empty body reason: ${step.empty_body_reason}`);
5975
6024
  if (step.missing_reason) writeLine(context.stdout, ` Missing reason: ${step.missing_reason}`);
6025
+ if (step.kind === "decision") {
6026
+ for (const option of step.next_action?.options || []) writeLine(context.stdout, ` ${display(option.id)}: ${display(option.label)}`);
6027
+ if (step.eligible_action_families?.length) writeLine(context.stdout, ` Eligible next steps: ${step.eligible_action_families.map(humanize).join(", ")}`);
6028
+ }
5976
6029
  }
5977
6030
 
5978
6031
  function renderProspectSequenceExport(payload, context) {
@@ -6265,6 +6318,7 @@ function renderOperatorQueue(payload, context) {
6265
6318
  }
6266
6319
 
6267
6320
  writeOperatorRows(context, queue);
6321
+ queue.forEach((row) => renderPlannerQuestion(row, context));
6268
6322
  }
6269
6323
 
6270
6324
  function renderInboxOpsQueue(payload, context) {
@@ -6321,6 +6375,17 @@ function renderOperatorNext(row, context) {
6321
6375
  if (!row) return writeLine(context.stdout, "No operator moves found.");
6322
6376
 
6323
6377
  writeOperatorRows(context, [row]);
6378
+ renderPlannerQuestion(row, context);
6379
+ }
6380
+
6381
+ function renderPlannerQuestion(row, context) {
6382
+ const action = row?.next_action;
6383
+ if (action?.type !== "answer_planner_question") return;
6384
+ writeLine(context.stdout, `Question (${row.id}): ${display(action.prompt)}`);
6385
+ for (const option of action.options || []) {
6386
+ writeLine(context.stdout, ` ${display(option.id)}: ${display(option.label)}`);
6387
+ }
6388
+ writeLine(context.stdout, `Answer: audienti operator answer ${row.id} --choice <id> or --answer <text>`);
6324
6389
  }
6325
6390
 
6326
6391
  function renderOperatorPlan(row, context) {
@@ -6329,6 +6394,7 @@ function renderOperatorPlan(row, context) {
6329
6394
  const nextAction = row.next_action || {};
6330
6395
  const cta = row.cta || {};
6331
6396
  const draft = row.operator_draft || {};
6397
+ renderPlannerQuestion(row, context);
6332
6398
 
6333
6399
  writeLine(context.stdout, "Static operator plan");
6334
6400
  writeLine(context.stdout, `Move: ${display(row.id)}`);
@@ -6594,6 +6660,22 @@ function renderAnalyticsMotions(payload, context) {
6594
6660
 
6595
6661
  writeMotionMixTable(payload?.prospect_mix, context);
6596
6662
  writeMotionContributionTable(payload?.motions, context);
6663
+ renderAdaptiveTreatments(payload?.adaptive_treatments, context);
6664
+ }
6665
+
6666
+ function renderAdaptiveTreatments(rollup, context) {
6667
+ if (!rollup) return;
6668
+ writeLine(context.stdout, "");
6669
+ writeLine(context.stdout, "Adaptive treatments (all time; exact outbound-event outcome links)");
6670
+ const rows = rollup.treatments || [];
6671
+ if (rows.length > 0) {
6672
+ writeAlignedTable(context, ["SOURCE MOTION", "APPROACH", "OUTBOUND", "REPLIES", "MEETING ASKS", "ACCEPTED", "QUESTIONS", "MEAN ANSWER (s)"], rows.map((row) => [
6673
+ display(row.source_motion_id), display(row.approach_digest).slice(0, 12), integerLabel(row.outbound_count),
6674
+ integerLabel(row.replies), integerLabel(row.meeting_asks), integerLabel(row.meetings_accepted),
6675
+ `${integerLabel(row.questions)}/${integerLabel(row.decision_count)}`, display(row.mean_answer_latency_seconds)
6676
+ ]));
6677
+ } else writeLine(context.stdout, "No adaptive treatments yet.");
6678
+ writeLine(context.stdout, `Adaptive prospects without an adaptive message link: ${integerLabel(rollup.unattributed?.replies)} replies, ${integerLabel(rollup.unattributed?.meetings_accepted)} accepted meetings.`);
6597
6679
  }
6598
6680
 
6599
6681
  function renderAnalyticsIcps(payload, context) {
@@ -7624,7 +7706,7 @@ const HELP_TOPICS = new Map([
7624
7706
  " audienti motions abm-companies <motn_id> list",
7625
7707
  " audienti motions abm-companies <motn_id> add <domain_or_linkedin_url>...",
7626
7708
  " audienti motions create --payload <file.json>",
7627
- " 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>]",
7709
+ " 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>] [--approach <text>]",
7628
7710
  " audienti motions update <motn_id> --payload <file.json>",
7629
7711
  " audienti motions add-tag <motn_id> <tag>",
7630
7712
  " audienti motions remove-tag <motn_id> <tag>",
@@ -7702,6 +7784,7 @@ const HELP_TOPICS = new Map([
7702
7784
  "",
7703
7785
  " Operator queue",
7704
7786
  " audienti operator next --plan",
7787
+ " audienti operator answer <row_id> (--choice <id> | --answer <text>)",
7705
7788
  " audienti operator next --done --note <text>",
7706
7789
  " audienti operator queue",
7707
7790
  " audienti operator failed-drafts",
@@ -9358,7 +9441,7 @@ const HELP_TOPICS = new Map([
9358
9441
  "Input shape:",
9359
9442
  " name: string",
9360
9443
  " premise: string",
9361
- " approach: string | optional",
9444
+ " approach: string | optional; a nonblank Approach guides adaptive planning, while blank uses the existing sequence",
9362
9445
  " kind: outbound | inbound | lopa | transition",
9363
9446
  " status: draft | active | paused | archived",
9364
9447
  " offer_id: offr_ prefix id",
@@ -9435,6 +9518,7 @@ const HELP_TOPICS = new Map([
9435
9518
  "Input shape:",
9436
9519
  " motn_id: motn_ prefix id",
9437
9520
  " status: draft | preparing | active | closing | paused | archived | optional",
9521
+ " approach: string | optional; a nonblank Approach guides adaptive planning; --approach \"\" restores the existing sequence",
9438
9522
  " principal_account_user_id: account user id | me | optional in payload mode",
9439
9523
  " list_id: list_ prefix id, numeric id, or null | optional in payload mode",
9440
9524
  " tags: comma-separated tag list | optional",
@@ -10617,6 +10701,7 @@ const HELP_TOPICS = new Map([
10617
10701
  " audienti operator failed-drafts [--json]",
10618
10702
  " audienti operator failed-drafts requeue (--all | <row_id> [row_id...])",
10619
10703
  " audienti operator outcome <row_id> --payload <file.json>",
10704
+ " audienti operator answer <row_id> (--choice <id> | --answer <text>)",
10620
10705
  "",
10621
10706
  "Status: read commands, failed draft requeue, and prospect next-move writeback implemented",
10622
10707
  "",
@@ -10707,6 +10792,23 @@ const HELP_TOPICS = new Map([
10707
10792
  " POST /api/v1/accounts/:account_id/operator/failed_drafts/requeue.json"
10708
10793
  ].join("\n")],
10709
10794
 
10795
+ ["operator answer", [
10796
+ "Usage:",
10797
+ " audienti operator answer <row_id> (--choice <id> | --answer <text>) [--json] [--account <acct_id>]",
10798
+ " --principal <account_user_id> selects the same authorized sender scope used by operator queue.",
10799
+ "",
10800
+ "Status: implemented",
10801
+ "",
10802
+ "Behavior:",
10803
+ " Refetches the exact current row, including questions that are not the next move, then sends its decision and fingerprints.",
10804
+ " Choose exactly one candidate id or a nonblank answer. Answers apply only to this decision; stale or already answered questions return 409.",
10805
+ " A 202 planning response records the answer; it does not authorize a provider send.",
10806
+ "",
10807
+ "API:",
10808
+ " GET /api/v1/accounts/:account_id/operator/row.json?row_id=:row_id",
10809
+ " POST /api/v1/accounts/:account_id/operator/answer.json"
10810
+ ].join("\n")],
10811
+
10710
10812
  ["operator outcome", [
10711
10813
  "Usage:",
10712
10814
  " audienti operator outcome <row_id> --payload <file.json> [--json] [--account <acct_id>]",