@audienti/cli 0.1.7 → 0.1.9

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.9] - 2026-07-15
8
+
9
+ ### Added
10
+
11
+ - Add `audienti prospects assign <prsp_id> [prsp_id...] --assigned-user <id|me|unassign>` for reassigning existing prospects from the CLI.
12
+ - Add `audienti users activity <account_user_id|me>` for inspecting one account user's outbound activity feed.
13
+ - Add `audienti prospects import-batch --file <csv|jsonl|json>` for starting multiple normal prospect imports with shared list, motion, and assignee defaults.
14
+ - Add `audienti prospects list --assigned-user unassigned` for finding prospects without an owner.
15
+
16
+ ## [0.1.8] - 2026-07-14
17
+
18
+ ### Added
19
+
20
+ - Add `audienti motions clone <motn_id> --name <text>` to clone a motion/play config through the API without copying people.
21
+ - Add `audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]` to transfer prospects between motions/plays.
22
+
7
23
  ## [0.1.7] - 2026-07-12
8
24
 
9
25
  ### Fixed
package/README.md CHANGED
@@ -56,6 +56,9 @@ audienti writer test-run <prsp_id>
56
56
  audienti motions analytics <motn_id>
57
57
  audienti prospects show <prsp_id> --json
58
58
  audienti prospects list --profiles
59
+ audienti prospects list --assigned-user unassigned
60
+ audienti prospects assign <prsp_id> --assigned-user me
61
+ audienti users activity me --window 7d
59
62
  audienti analytics prospects --window 24h
60
63
  audienti analytics users --user me --window 30d
61
64
  audienti analytics visibility --window 24h --user me
@@ -126,6 +129,22 @@ audienti prospects add-profile <prsp_id> --url https://www.linkedin.com/in/examp
126
129
  audienti prospects report-bad-profile <prsp_id> <prof_id>
127
130
  ```
128
131
 
132
+ To reassign or clear ownership for existing prospects:
133
+
134
+ ```bash
135
+ audienti prospects assign <prsp_id> --assigned-user <account_user_id|me>
136
+ audienti prospects assign <prsp_id> --assigned-user unassign
137
+ ```
138
+
139
+ To import multiple LinkedIn people through the same per-prospect import path:
140
+
141
+ ```bash
142
+ audienti prospects import-batch --file prospects.csv --motion <motn_id> --assigned-user me
143
+ ```
144
+
145
+ CSV files should include a `linkedin_url` or `url` header. Optional row columns
146
+ `list_id`, `motion_id`, and `assigned_user_id` override command defaults.
147
+
129
148
  ## Compatibility
130
149
 
131
150
  The CLI talks to the versioned Audienti `/api/v1` contract at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Agent-first command-line client for Audienti.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,6 +51,10 @@ audienti help agent-workflows
51
51
  ```bash
52
52
  audienti help agent-workflows
53
53
  audienti prospects list --query "name or company" --wide --json
54
+ audienti prospects list --assigned-user unassigned --json
55
+ audienti prospects assign <prsp_id> --assigned-user me --json
56
+ audienti users activity me --window 7d --json
57
+ audienti prospects import-batch --file prospects.csv --motion <motn_id> --assigned-user me --json
54
58
  audienti lists create --name "Target list" --json
55
59
  audienti operator next --json
56
60
  audienti operator next --plan
package/src/api-client.js CHANGED
@@ -43,6 +43,10 @@ export class AudientiClient {
43
43
  return this.requestJson(accountPath(accountId, ["users"]));
44
44
  }
45
45
 
46
+ userActivity(accountId, userId, query = {}) {
47
+ return this.requestJson(accountPath(accountId, ["operations", "users", userId, "activity"], query));
48
+ }
49
+
46
50
  offers(accountId) {
47
51
  return this.requestJson(accountPath(accountId, ["offers"]));
48
52
  }
@@ -130,6 +134,20 @@ export class AudientiClient {
130
134
  });
131
135
  }
132
136
 
137
+ cloneMotion(accountId, motionId, body) {
138
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "clone"]), {
139
+ method: "POST",
140
+ body
141
+ });
142
+ }
143
+
144
+ moveMotionProspects(accountId, motionId, body) {
145
+ return this.requestJson(accountPath(accountId, ["motions", motionId, "move_prospects"]), {
146
+ method: "POST",
147
+ body
148
+ });
149
+ }
150
+
133
151
  motionStatus(accountId, motionId) {
134
152
  return this.requestJson(accountPath(accountId, ["motions", motionId, "status"]));
135
153
  }
@@ -153,6 +171,13 @@ export class AudientiClient {
153
171
  return this.requestJson(accountPath(accountId, ["prospects", prospectId]));
154
172
  }
155
173
 
174
+ assignProspects(accountId, body) {
175
+ return this.requestJson(accountPath(accountId, ["prospects", "assign"]), {
176
+ method: "POST",
177
+ body
178
+ });
179
+ }
180
+
156
181
  prospectTimeline(accountId, prospectId, query = {}) {
157
182
  return this.requestJson(accountPath(accountId, ["prospects", prospectId, "timeline"], query));
158
183
  }
package/src/cli.js CHANGED
@@ -29,8 +29,13 @@ const PROSPECTS_ADD_NOTE_USAGE = "Usage: audienti prospects add-note <prsp_id> (
29
29
  const PROSPECTS_ADD_STEER_USAGE = "Usage: audienti prospects add-steer <prsp_id> (--message <text> [--engagement-type <key>] | --payload <file.json>) [--json] [--account <acct_id>]";
30
30
  const PROSPECTS_ADD_PROFILE_USAGE = "Usage: audienti prospects add-profile <prsp_id> --url <profile_url|email|phone> [--json] [--account <acct_id>]";
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
+ const PROSPECTS_ASSIGN_USAGE = "Usage: audienti prospects assign <prsp_id> [prsp_id...] --assigned-user <id|me|unassign> [--json] [--account <acct_id>]";
33
+ const PROSPECTS_IMPORT_BATCH_USAGE = "Usage: audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--assigned-user <id|me>] [--json] [--account <acct_id>]";
34
+ const USERS_ACTIVITY_USAGE = "Usage: audienti users activity <account_user_id|me> [--mode <actor|account_usage>] [--window <24h|7d|30d>] [--platform <linkedin|email|gmail>] [--query <text>] [--limit <n>] [--page <n>] [--json] [--account <acct_id>]";
32
35
  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
36
  const MOTIONS_ANALYTICS_USAGE = "Usage: audienti motions analytics <motn_id> [--window 30d] [--json] [--account <acct_id>]";
37
+ const MOTIONS_CLONE_USAGE = "Usage: audienti motions clone <motn_id> --name <text> [--json] [--account <acct_id>]";
38
+ 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
39
  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
40
  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
41
  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>]";
@@ -115,6 +120,7 @@ async function dispatch(argv, context) {
115
120
  if (normalizedResource === "accounts" && action === "list") return accountsList(rest, context, { accountOverride });
116
121
  if (normalizedResource === "accounts" && action === "select") return accountsSelect(rest, context);
117
122
  if (normalizedResource === "users" && action === "list") return usersList(rest, context, { accountOverride });
123
+ if (normalizedResource === "users" && action === "activity") return usersActivity(rest, context, { accountOverride });
118
124
  if (normalizedResource === "offers" && action === "list") return offersList(rest, context, { accountOverride });
119
125
  if (normalizedResource === "offers" && action === "create") return offersCreate(rest, context, { accountOverride });
120
126
  if (normalizedResource === "icps" && action === "list") return icpsList(rest, context, { accountOverride });
@@ -135,8 +141,11 @@ async function dispatch(argv, context) {
135
141
  if (normalizedResource === "motions" && action === "prospects") return motionsProspects(rest, context, { accountOverride });
136
142
  if (normalizedResource === "motions" && action === "add-prospects") return motionsAddProspects(rest, context, { accountOverride });
137
143
  if (normalizedResource === "motions" && action === "create") return motionsCreate(rest, context, { accountOverride });
144
+ if (normalizedResource === "motions" && action === "clone") return motionsClone(rest, context, { accountOverride });
145
+ if (normalizedResource === "motions" && action === "move-prospects") return motionsMoveProspects(rest, context, { accountOverride });
138
146
  if (normalizedResource === "prospects" && action === "list") return prospectsList(rest, context, { accountOverride });
139
147
  if (normalizedResource === "prospects" && action === "show") return prospectsShow(rest, context, { accountOverride });
148
+ if (normalizedResource === "prospects" && action === "assign") return prospectsAssign(rest, context, { accountOverride });
140
149
  if (normalizedResource === "prospects" && action === "timeline") return prospectsTimeline(rest, context, { accountOverride });
141
150
  if (normalizedResource === "prospects" && action === "message-types") return prospectsMessageTypes(rest, context, { accountOverride });
142
151
  if (normalizedResource === "prospects" && action === "write") return prospectsWrite(rest, context, { accountOverride });
@@ -147,6 +156,7 @@ async function dispatch(argv, context) {
147
156
  if (normalizedResource === "prospects" && action === "sequence-preview") return prospectsSequencePreview(rest, context, { accountOverride });
148
157
  if (normalizedResource === "prospects" && action === "sequence-export") return prospectsSequenceExport(rest, context, { accountOverride });
149
158
  if (normalizedResource === "prospects" && action === "import") return prospectsImport(rest, context, { accountOverride });
159
+ if (normalizedResource === "prospects" && action === "import-batch") return prospectsImportBatch(rest, context, { accountOverride });
150
160
  if (normalizedResource === "prospects" && action === "import-status") return prospectsImportStatus(rest, context, { accountOverride });
151
161
  if (normalizedResource === "writer" && action === "test-run") return writerTestRun(rest, context, { accountOverride });
152
162
  if (normalizedResource === "tools" && action === "get") return toolsGet(rest, context, { accountOverride });
@@ -395,6 +405,32 @@ async function usersList(args, context, { accountOverride } = {}) {
395
405
  renderUsers(users, context);
396
406
  }
397
407
 
408
+ async function usersActivity(args, context, { accountOverride } = {}) {
409
+ const { values, positionals } = parseCommandArgs(args, {
410
+ ...jsonOptions(),
411
+ mode: { type: "string" },
412
+ window: { type: "string" },
413
+ platform: { type: "string" },
414
+ query: { type: "string" },
415
+ limit: { type: "string" },
416
+ page: { type: "string" }
417
+ });
418
+ if (positionals.length !== 1) throw new CommandError(USERS_ACTIVITY_USAGE);
419
+
420
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
421
+ const payload = await client.userActivity(accountId, positionals[0], compactObject({
422
+ mode: values.mode,
423
+ window: values.window,
424
+ platform: values.platform,
425
+ query: values.query,
426
+ limit: values.limit,
427
+ page: values.page
428
+ }));
429
+ if (values.json) return writeJson(context.stdout, payload);
430
+
431
+ renderUserActivity(payload, context);
432
+ }
433
+
398
434
  async function offersList(args, context, { accountOverride } = {}) {
399
435
  const { values, positionals } = parseCommandArgs(args, jsonOptions());
400
436
  if (positionals.length > 0) throw new CommandError("Usage: audienti offers list [--json] [--account <acct_id>]");
@@ -782,6 +818,52 @@ async function motionsCreate(args, context, { accountOverride } = {}) {
782
818
  renderMotion(created, context);
783
819
  }
784
820
 
821
+ async function motionsClone(args, context, { accountOverride } = {}) {
822
+ const { values, positionals } = parseCommandArgs(args, {
823
+ ...jsonOptions(),
824
+ name: { type: "string" }
825
+ });
826
+ if (positionals.length !== 1 || !values.name) {
827
+ throw new CommandError(MOTIONS_CLONE_USAGE);
828
+ }
829
+
830
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
831
+ const cloned = await client.cloneMotion(accountId, positionals[0], {
832
+ motion: {
833
+ name: values.name
834
+ }
835
+ });
836
+ if (values.json) return writeJson(context.stdout, cloned);
837
+
838
+ writeLine(context.stdout, `Cloned motion ${display(positionals[0])} as ${display(cloned?.name)} (${display(cloned?.prefix_id)}).`);
839
+ renderMotion(cloned, context);
840
+ }
841
+
842
+ async function motionsMoveProspects(args, context, { accountOverride } = {}) {
843
+ const { values, positionals } = parseCommandArgs(args, {
844
+ ...jsonOptions(),
845
+ target: { type: "string" }
846
+ });
847
+ if (positionals.length < 2 || !values.target) {
848
+ throw new CommandError(MOTIONS_MOVE_PROSPECTS_USAGE);
849
+ }
850
+
851
+ const [sourceMotionId, ...prospectIds] = positionals;
852
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
853
+ const payload = await client.moveMotionProspects(accountId, sourceMotionId, {
854
+ target_motion_id: values.target,
855
+ prospect_ids: prospectIds
856
+ });
857
+ if (values.json) return writeJson(context.stdout, payload);
858
+
859
+ const moved = Number(payload?.moved || 0);
860
+ const failed = Array.isArray(payload?.failed) ? payload.failed.length : 0;
861
+ writeLine(context.stdout, `Moved ${moved} prospects from ${display(sourceMotionId)} to ${display(values.target)}.`);
862
+ if (failed > 0) {
863
+ writeLine(context.stdout, `${failed} prospects failed.`);
864
+ }
865
+ }
866
+
785
867
  async function prospectsList(args, context, { accountOverride } = {}) {
786
868
  const { values, positionals } = parseCommandArgs(args, {
787
869
  ...jsonOptions(),
@@ -832,6 +914,36 @@ async function prospectsList(args, context, { accountOverride } = {}) {
832
914
  renderProspects(payload, context, { wide: values.wide || values.all, profiles: values.profiles });
833
915
  }
834
916
 
917
+ async function prospectsAssign(args, context, { accountOverride } = {}) {
918
+ const { values, positionals } = parseCommandArgs(args, {
919
+ ...jsonOptions(),
920
+ "assigned-user": { type: "string" }
921
+ });
922
+ if (positionals.length < 1 || !values["assigned-user"]) {
923
+ throw new CommandError(PROSPECTS_ASSIGN_USAGE);
924
+ }
925
+
926
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
927
+ const { payload, rejected } = await performBulkMutation(() =>
928
+ client.assignProspects(accountId, {
929
+ prospect_ids: positionals,
930
+ assigned_user_id: values["assigned-user"]
931
+ }));
932
+ if (values.json) {
933
+ writeJson(context.stdout, payload);
934
+ return rejected ? 1 : 0;
935
+ }
936
+
937
+ const successLabel = values["assigned-user"] === "unassign" ?
938
+ `Unassigned ${successCount(payload)} prospects.` :
939
+ `Assigned ${successCount(payload)} prospects to ${display(values["assigned-user"])}.`;
940
+ renderBulkMutationResult(payload, context, {
941
+ successLabel,
942
+ zeroSuccessLabel: "No prospects were assigned."
943
+ });
944
+ return rejected ? 1 : 0;
945
+ }
946
+
835
947
  async function prospectsShow(args, context, { accountOverride } = {}) {
836
948
  const { values, positionals } = parseCommandArgs(args, jsonOptions());
837
949
  if (positionals.length !== 1) throw new CommandError("Usage: audienti prospects show <prsp_id> [--json] [--account <acct_id>]");
@@ -1245,6 +1357,66 @@ async function prospectsImport(args, context, { accountOverride } = {}) {
1245
1357
  renderProspectImportStarted(payload, context);
1246
1358
  }
1247
1359
 
1360
+ async function prospectsImportBatch(args, context, { accountOverride } = {}) {
1361
+ const { values, positionals } = parseCommandArgs(args, {
1362
+ ...jsonOptions(),
1363
+ file: { type: "string" },
1364
+ list: { type: "string" },
1365
+ motion: { type: "string" },
1366
+ "assigned-user": { type: "string" }
1367
+ });
1368
+ if (positionals.length > 0 || !values.file) {
1369
+ throw new CommandError(PROSPECTS_IMPORT_BATCH_USAGE);
1370
+ }
1371
+
1372
+ const rows = await readProspectImportBatchFile(values.file);
1373
+ if (rows.length === 0) throw new CommandError("Import batch file did not contain any prospects.");
1374
+
1375
+ const { client, accountId } = await requireAccountContext(context, { accountOverride });
1376
+ const result = {
1377
+ summary: {
1378
+ total: rows.length,
1379
+ started: 0,
1380
+ failed: 0
1381
+ },
1382
+ imports: [],
1383
+ failed: []
1384
+ };
1385
+
1386
+ for (const row of rows) {
1387
+ const body = compactObject({
1388
+ linkedin_url: row.linkedin_url,
1389
+ list_id: row.list_id || values.list,
1390
+ motion_id: row.motion_id || values.motion,
1391
+ assigned_user_id: row.assigned_user_id || values["assigned-user"]
1392
+ });
1393
+
1394
+ try {
1395
+ const payload = await client.prospectImport(accountId, body);
1396
+ result.imports.push(payload);
1397
+ result.summary.started += 1;
1398
+ } catch (error) {
1399
+ if (!(error instanceof ApiError)) throw error;
1400
+
1401
+ result.failed.push({
1402
+ row: row.row,
1403
+ linkedin_url: row.linkedin_url,
1404
+ status: error.status,
1405
+ error: error.body?.error || error.message
1406
+ });
1407
+ result.summary.failed += 1;
1408
+ }
1409
+ }
1410
+
1411
+ if (values.json) {
1412
+ writeJson(context.stdout, result);
1413
+ return result.summary.failed > 0 ? 1 : 0;
1414
+ }
1415
+
1416
+ renderProspectImportBatchResult(result, context);
1417
+ return result.summary.failed > 0 ? 1 : 0;
1418
+ }
1419
+
1248
1420
  async function prospectsImportStatus(args, context, { accountOverride } = {}) {
1249
1421
  const { values, positionals } = parseCommandArgs(args, jsonOptions());
1250
1422
  if (positionals.length !== 1) {
@@ -1934,6 +2106,113 @@ async function readJsonPayload(filePath) {
1934
2106
  }
1935
2107
  }
1936
2108
 
2109
+ async function readProspectImportBatchFile(filePath) {
2110
+ let contents;
2111
+ try {
2112
+ contents = await readFile(filePath, "utf8");
2113
+ } catch (error) {
2114
+ throw new CommandError(`Could not read import batch file ${filePath}: ${error.message}`);
2115
+ }
2116
+
2117
+ return parseProspectImportBatch(contents, filePath);
2118
+ }
2119
+
2120
+ function parseProspectImportBatch(contents, filePath = "batch file") {
2121
+ const trimmed = String(contents || "").trim();
2122
+ if (!trimmed) return [];
2123
+
2124
+ if (trimmed.startsWith("[") || (trimmed.startsWith("{") && !trimmed.includes("\n"))) {
2125
+ try {
2126
+ const parsed = JSON.parse(trimmed);
2127
+ return normalizeImportBatchRows(Array.isArray(parsed) ? parsed : [parsed], filePath);
2128
+ } catch (error) {
2129
+ throw new CommandError(`Invalid JSON import batch in ${filePath}: ${error.message}`);
2130
+ }
2131
+ }
2132
+
2133
+ const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2134
+ if (lines.length === 0) return [];
2135
+
2136
+ if (looksLikeCsvHeader(lines[0])) {
2137
+ return parseProspectImportCsv(lines, filePath);
2138
+ }
2139
+
2140
+ const rows = lines.map((line, index) => {
2141
+ if (line.startsWith("{")) {
2142
+ try {
2143
+ return { ...JSON.parse(line), row: index + 1 };
2144
+ } catch (error) {
2145
+ throw new CommandError(`Invalid JSONL row ${index + 1} in ${filePath}: ${error.message}`);
2146
+ }
2147
+ }
2148
+
2149
+ return { linkedin_url: line, row: index + 1 };
2150
+ });
2151
+
2152
+ return normalizeImportBatchRows(rows, filePath);
2153
+ }
2154
+
2155
+ function looksLikeCsvHeader(line) {
2156
+ const headers = parseCsvLine(line).map((header) => header.trim().toLowerCase());
2157
+ return headers.includes("linkedin_url") || headers.includes("url");
2158
+ }
2159
+
2160
+ function parseProspectImportCsv(lines, filePath) {
2161
+ const headers = parseCsvLine(lines[0]).map((header) => header.trim());
2162
+ const rows = lines.slice(1).map((line, index) => {
2163
+ const values = parseCsvLine(line);
2164
+ return headers.reduce((row, header, headerIndex) => {
2165
+ row[header] = values[headerIndex] || "";
2166
+ return row;
2167
+ }, { row: index + 1 });
2168
+ });
2169
+
2170
+ return normalizeImportBatchRows(rows, filePath);
2171
+ }
2172
+
2173
+ function parseCsvLine(line) {
2174
+ const values = [];
2175
+ let current = "";
2176
+ let inQuotes = false;
2177
+
2178
+ for (let index = 0; index < line.length; index += 1) {
2179
+ const character = line[index];
2180
+ const next = line[index + 1];
2181
+
2182
+ if (character === "\"" && inQuotes && next === "\"") {
2183
+ current += "\"";
2184
+ index += 1;
2185
+ } else if (character === "\"") {
2186
+ inQuotes = !inQuotes;
2187
+ } else if (character === "," && !inQuotes) {
2188
+ values.push(current);
2189
+ current = "";
2190
+ } else {
2191
+ current += character;
2192
+ }
2193
+ }
2194
+
2195
+ values.push(current);
2196
+ return values.map((value) => value.trim());
2197
+ }
2198
+
2199
+ function normalizeImportBatchRows(rows, filePath) {
2200
+ return rows.map((row, index) => {
2201
+ const rowNumber = row?.row || index + 1;
2202
+ const normalized = typeof row === "string" ? { linkedin_url: row } : row;
2203
+ const linkedinUrl = normalized?.linkedin_url || normalized?.url;
2204
+ if (!linkedinUrl) throw new CommandError(`Missing linkedin_url on row ${rowNumber} in ${filePath}.`);
2205
+
2206
+ return compactObject({
2207
+ row: rowNumber,
2208
+ linkedin_url: linkedinUrl,
2209
+ list_id: normalized.list_id,
2210
+ motion_id: normalized.motion_id,
2211
+ assigned_user_id: normalized.assigned_user_id || normalized.assigned_user
2212
+ });
2213
+ });
2214
+ }
2215
+
1937
2216
  function writeLine(stream, text = "") {
1938
2217
  stream.write(`${text}\n`);
1939
2218
  }
@@ -1979,6 +2258,41 @@ function renderUsers(users, context) {
1979
2258
  }
1980
2259
  }
1981
2260
 
2261
+ function renderUserActivity(payload, context) {
2262
+ const accountUser = payload?.account_user || {};
2263
+ const summary = payload?.summary || {};
2264
+ const events = Array.isArray(payload?.events) ? payload.events : [];
2265
+ const pagination = payload?.pagination || {};
2266
+
2267
+ writeLine(context.stdout, `User: ${display(accountUser.name || accountUser.email)} (${display(accountUser.id)})`);
2268
+ writeLine(context.stdout, `Window actions: ${display(summary.window_count, 0)}`);
2269
+ if (pagination.page || pagination.pages) {
2270
+ writeLine(context.stdout, `Page: ${display(pagination.page, 1)} of ${display(pagination.pages, 1)}`);
2271
+ }
2272
+ renderCountRows(context, "By platform", summary.by_platform);
2273
+ renderCountRows(context, "By action", summary.by_key);
2274
+
2275
+ if (events.length === 0) return writeLine(context.stdout, "No activity events found.");
2276
+
2277
+ writeLine(context.stdout, "TIME\tACTION\tPLATFORM\tPROSPECT\tCOMPANY\tDETAILS");
2278
+ for (const event of events) {
2279
+ writeLine(context.stdout, [
2280
+ display(event.occurred_at),
2281
+ display(event.action_label || event.key),
2282
+ display(event.platform),
2283
+ display(event.prospect?.name || event.prospect?.display_name || event.prospect?.prefix_id),
2284
+ display(event.prospect?.company),
2285
+ display(event.details)
2286
+ ].join("\t"));
2287
+ }
2288
+ }
2289
+
2290
+ function renderCountRows(context, label, rows) {
2291
+ if (!Array.isArray(rows) || rows.length === 0) return;
2292
+
2293
+ writeLine(context.stdout, `${label}: ${rows.map((row) => `${display(row.label || row.key)} ${display(row.count, 0)}`).join(" | ")}`);
2294
+ }
2295
+
1982
2296
  function renderOffers(offers, context) {
1983
2297
  if (!Array.isArray(offers) || offers.length === 0) return writeLine(context.stdout, "No offers found.");
1984
2298
 
@@ -2514,6 +2828,30 @@ function renderProspectImportStarted(payload, context) {
2514
2828
  if (payload?.prefix_id) writeLine(context.stdout, `Run \`audienti prospects import-status ${payload.prefix_id}\` to check completion.`);
2515
2829
  }
2516
2830
 
2831
+ function renderProspectImportBatchResult(result, context) {
2832
+ const imports = Array.isArray(result?.imports) ? result.imports : [];
2833
+ const failed = Array.isArray(result?.failed) ? result.failed : [];
2834
+
2835
+ writeLine(context.stdout, `Started ${display(result?.summary?.started, 0)} prospect imports.`);
2836
+ writeLine(context.stdout, `Failures: ${display(result?.summary?.failed, 0)}`);
2837
+
2838
+ if (imports.length > 0) {
2839
+ writeLine(context.stdout, "IMPORT ID\tPROSPECT\tPROSPECT ID\tSTATUS");
2840
+ for (const payload of imports) {
2841
+ writeLine(context.stdout, [
2842
+ display(payload?.prefix_id),
2843
+ display(payload?.prospect?.display_name || payload?.prospect?.name),
2844
+ display(payload?.prospect?.prefix_id),
2845
+ display(payload?.status)
2846
+ ].join("\t"));
2847
+ }
2848
+ }
2849
+
2850
+ for (const row of failed) {
2851
+ writeLine(context.stdout, `- row ${display(row.row)} ${display(row.linkedin_url)}: ${display(row.error, "failed")}`);
2852
+ }
2853
+ }
2854
+
2517
2855
  function renderProspectImportStatus(payload, context) {
2518
2856
  writeLine(context.stdout, `Import: ${display(payload?.prefix_id)}`);
2519
2857
  writeLine(context.stdout, `Status: ${display(payload?.status)}`);
@@ -3263,6 +3601,7 @@ const HELP_TOPICS = new Map([
3263
3601
  " audienti auth status",
3264
3602
  " audienti config list",
3265
3603
  " audienti users list",
3604
+ " audienti users activity <account_user_id|me>",
3266
3605
  "",
3267
3606
  " Motions / plays",
3268
3607
  " audienti motions list",
@@ -3270,13 +3609,17 @@ const HELP_TOPICS = new Map([
3270
3609
  " audienti motions analytics <motn_id>",
3271
3610
  " audienti motions prospects <motn_id>",
3272
3611
  " audienti motions create --payload <file.json>",
3612
+ " audienti motions clone <motn_id> --name <text>",
3613
+ " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]",
3273
3614
  " Tip: `plays` is accepted anywhere `motions` is accepted.",
3274
3615
  "",
3275
3616
  " Prospects",
3276
3617
  " audienti prospects list [filters]",
3277
3618
  " audienti prospects show <prsp_id>",
3619
+ " audienti prospects assign <prsp_id> --assigned-user <id|me|unassign>",
3278
3620
  " audienti prospects timeline <prsp_id>",
3279
3621
  " audienti prospects import <linkedin_url> [--motion <motn_id>]",
3622
+ " audienti prospects import-batch --file <csv|jsonl|json>",
3280
3623
  " audienti prospects add-note <prsp_id> --message <text>",
3281
3624
  " audienti prospects add-profile <prsp_id> --url <profile_url|email|phone>",
3282
3625
  "",
@@ -3453,6 +3796,7 @@ const HELP_TOPICS = new Map([
3453
3796
  ["users", [
3454
3797
  "Usage:",
3455
3798
  " audienti users list [--json]",
3799
+ " audienti users activity <account_user_id|me> [--json]",
3456
3800
  "",
3457
3801
  "Status: implemented",
3458
3802
  "",
@@ -3481,6 +3825,25 @@ const HELP_TOPICS = new Map([
3481
3825
  " current: boolean"
3482
3826
  ].join("\n")],
3483
3827
 
3828
+ ["users activity", [
3829
+ "Usage:",
3830
+ ` ${USERS_ACTIVITY_USAGE.slice("Usage: ".length)}`,
3831
+ "",
3832
+ "Status: implemented",
3833
+ "",
3834
+ "Purpose:",
3835
+ " Inspect one workspace user's outbound activity feed and action summary.",
3836
+ "",
3837
+ "Input shape:",
3838
+ " account_user_id: integer account user id, or me for the authenticated token user",
3839
+ " mode: actor | account_usage",
3840
+ " window: 24h | 7d | 30d",
3841
+ " platform: linkedin | email | gmail",
3842
+ "",
3843
+ "API:",
3844
+ " GET /api/v1/accounts/:account_id/operations/users/:user_id/activity.json"
3845
+ ].join("\n")],
3846
+
3484
3847
  ["offers", [
3485
3848
  "Usage:",
3486
3849
  " audienti offers list [--json]",
@@ -3842,8 +4205,10 @@ const HELP_TOPICS = new Map([
3842
4205
  " audienti motions prospects <motn_id> [--json]",
3843
4206
  " audienti motions add-prospects <motn_id> <prsp_id> [prsp_id...] [--json]",
3844
4207
  " audienti motions create --payload <file.json> [--json]",
4208
+ " audienti motions clone <motn_id> --name <text> [--json]",
4209
+ " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...] [--json]",
3845
4210
  "",
3846
- "Status: read, create, status, and prospect attachment commands implemented",
4211
+ "Status: read, create, clone, status, and prospect attachment commands implemented",
3847
4212
  "",
3848
4213
  "CLI synonym:",
3849
4214
  " `plays` is accepted anywhere `motions` is accepted",
@@ -4017,10 +4382,67 @@ const HELP_TOPICS = new Map([
4017
4382
  " Use `audienti offers list`, `audienti icps list`, and `audienti users list` to resolve valid ids before calling this command."
4018
4383
  ].join("\n")],
4019
4384
 
4385
+ ["motions clone", [
4386
+ "Usage:",
4387
+ ` ${MOTIONS_CLONE_USAGE.slice("Usage: ".length)}`,
4388
+ "",
4389
+ "Status: implemented",
4390
+ "",
4391
+ "Purpose:",
4392
+ " Clone one motion or play's configuration under a new name without copying its people.",
4393
+ "",
4394
+ "Input shape:",
4395
+ " motn_id: motn_ prefix id",
4396
+ " name: new motion name",
4397
+ "",
4398
+ "Behavior:",
4399
+ " Copies the motion kind, offer, ICP, principal, premise, approach, targeting profile, suppression policy, secondary roles, and active signal rows.",
4400
+ " The clone starts as draft with a new empty backing list.",
4401
+ "",
4402
+ "API:",
4403
+ " POST /api/v1/accounts/:account_id/motions/:id/clone.json",
4404
+ "",
4405
+ "JSON body:",
4406
+ " {",
4407
+ " \"motion\": {",
4408
+ " \"name\": \"Wine Campaign Restaurant Operators\"",
4409
+ " }",
4410
+ " }"
4411
+ ].join("\n")],
4412
+
4413
+ ["motions move-prospects", [
4414
+ "Usage:",
4415
+ ` ${MOTIONS_MOVE_PROSPECTS_USAGE.slice("Usage: ".length)}`,
4416
+ "",
4417
+ "Status: implemented",
4418
+ "",
4419
+ "Purpose:",
4420
+ " Move prospects out of one motion or play and into another motion or play.",
4421
+ "",
4422
+ "Input shape:",
4423
+ " source_motn_id: source motn_ prefix id",
4424
+ " target_motn_id: target motn_ prefix id",
4425
+ " prsp_id: one or more prospect prefix ids",
4426
+ "",
4427
+ "Behavior:",
4428
+ " Move removes each selected prospect from the source motion and source backing list, then adds it to the target motion and target backing list.",
4429
+ " Copy is intentionally not exposed until multi-motion membership exists.",
4430
+ "",
4431
+ "API:",
4432
+ " POST /api/v1/accounts/:account_id/motions/:id/move_prospects.json",
4433
+ "",
4434
+ "JSON body:",
4435
+ " {",
4436
+ " \"target_motion_id\": \"motn_target\",",
4437
+ " \"prospect_ids\": [\"prsp_one\", \"prsp_two\"]",
4438
+ " }"
4439
+ ].join("\n")],
4440
+
4020
4441
  ["prospects", [
4021
4442
  "Usage:",
4022
4443
  " audienti prospects list [--json] [filters]",
4023
4444
  " audienti prospects show <prsp_id> [--json]",
4445
+ " audienti prospects assign <prsp_id> [prsp_id...] --assigned-user <id|me|unassign> [--json]",
4024
4446
  " audienti prospects timeline <prsp_id> [--json]",
4025
4447
  " audienti prospects message-types <prsp_id> [--json]",
4026
4448
  " audienti prospects write <prsp_id> --type <surface_key> [--json]",
@@ -4031,9 +4453,10 @@ const HELP_TOPICS = new Map([
4031
4453
  " audienti prospects sequence-preview <prsp_id> [--json]",
4032
4454
  " audienti prospects sequence-export <prsp_id> [--csv]",
4033
4455
  " audienti prospects import <linkedin_url> [--list <list_id>] [--motion <motn_id>] [--json]",
4456
+ " audienti prospects import-batch --file <csv|jsonl|json> [--list <list_id>] [--motion <motn_id>] [--json]",
4034
4457
  " audienti prospects import-status <primp_id> [--json]",
4035
4458
  "",
4036
- "Status: read commands, per-prospect draft preview, sequence preview, and import implemented; disposition planned",
4459
+ "Status: read commands, assignment, per-prospect draft preview, sequence preview, and import implemented; disposition planned",
4037
4460
  "",
4038
4461
  "Filters:",
4039
4462
  " --query <text>",
@@ -4043,7 +4466,7 @@ const HELP_TOPICS = new Map([
4043
4466
  " --play <motn_id>",
4044
4467
  " --list <list_id>",
4045
4468
  " --stage <stage>",
4046
- " --assigned-user <account_user_id|me>",
4469
+ " --assigned-user <account_user_id|me|unassigned>",
4047
4470
  " --limit <n>",
4048
4471
  " --page <n>",
4049
4472
  " --offset <n>",
@@ -4071,7 +4494,7 @@ const HELP_TOPICS = new Map([
4071
4494
  " --play <motn_id> Filter to a play using the same motion relationship",
4072
4495
  " --list <list_id> Filter to a prospect list",
4073
4496
  " --stage <stage> Filter to a pipeline stage",
4074
- " --assigned-user <id|me> Filter by assigned account user",
4497
+ " --assigned-user <id|me|unassigned> Filter by assigned account user",
4075
4498
  " --limit <n> Max rows for one page; with --all it caps total rows up to 1000",
4076
4499
  " --page <n> 1-based page number",
4077
4500
  " --offset <n> Row offset for manual pagination",
@@ -4098,9 +4521,33 @@ const HELP_TOPICS = new Map([
4098
4521
  "",
4099
4522
  "Examples:",
4100
4523
  " audienti prospects list --stage identified --page 2 --limit 50",
4524
+ " audienti prospects list --assigned-user unassigned",
4101
4525
  " audienti prospects list --all --csv"
4102
4526
  ].join("\n")],
4103
4527
 
4528
+ ["prospects assign", [
4529
+ "Usage:",
4530
+ ` ${PROSPECTS_ASSIGN_USAGE.slice("Usage: ".length)}`,
4531
+ "",
4532
+ "Status: implemented",
4533
+ "",
4534
+ "Input shape:",
4535
+ " prsp_id: one or more prsp_ prefix ids",
4536
+ " assigned_user_id: account user id, me, or unassign",
4537
+ "",
4538
+ "Behavior:",
4539
+ " Updates AccountProspect.assigned_to_account_user_id for existing account prospects without changing motion or list membership.",
4540
+ "",
4541
+ "API:",
4542
+ " POST /api/v1/accounts/:account_id/prospects/assign.json",
4543
+ "",
4544
+ "JSON body:",
4545
+ " {",
4546
+ " \"prospect_ids\": [\"prsp_abc123\", \"prsp_def456\"],",
4547
+ " \"assigned_user_id\": \"me\"",
4548
+ " }"
4549
+ ].join("\n")],
4550
+
4104
4551
  ["prospects show", [
4105
4552
  "Usage:",
4106
4553
  " audienti prospects show <prsp_id> [--json] [--account <acct_id>]",
@@ -4426,6 +4873,28 @@ const HELP_TOPICS = new Map([
4426
4873
  " }"
4427
4874
  ].join("\n")],
4428
4875
 
4876
+ ["prospects import-batch", [
4877
+ "Usage:",
4878
+ ` ${PROSPECTS_IMPORT_BATCH_USAGE.slice("Usage: ".length)}`,
4879
+ "",
4880
+ "Status: implemented",
4881
+ "",
4882
+ "Input shape:",
4883
+ " file: CSV with linkedin_url/url header, JSON array, JSONL objects, or newline-delimited LinkedIn URLs",
4884
+ " list_id: list_ prefix id | optional default for every row",
4885
+ " motn_id: motn_ prefix id | optional default for every row",
4886
+ " assigned_user_id: account user id or me | optional default for every row",
4887
+ "",
4888
+ "CSV columns:",
4889
+ " linkedin_url or url, list_id, motion_id, assigned_user_id",
4890
+ "",
4891
+ "Behavior:",
4892
+ " Starts one normal prospect import per row. Row-level list_id, motion_id, and assigned_user_id override command defaults.",
4893
+ "",
4894
+ "API:",
4895
+ " POST /api/v1/accounts/:account_id/prospect_imports.json"
4896
+ ].join("\n")],
4897
+
4429
4898
  ["prospects import-status", [
4430
4899
  "Usage:",
4431
4900
  " audienti prospects import-status <primp_id> [--json] [--account <acct_id>]",
@@ -4762,17 +5231,22 @@ const HELP_TOPICS = new Map([
4762
5231
  "",
4763
5232
  "2. Create a motion or play",
4764
5233
  " audienti motions create --payload <file.json>",
5234
+ " audienti motions clone <motn_id> --name \"New subset motion\"",
5235
+ " audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]",
4765
5236
  " audienti motions status <motn_id>",
4766
5237
  "",
4767
5238
  "3. Add a new prospect from LinkedIn and poll enrichment",
4768
5239
  " audienti lists create --name \"Target list\"",
4769
5240
  " audienti prospects import https://www.linkedin.com/in/example --list <list_id> --assigned-user me",
5241
+ " audienti prospects import-batch --file prospects.csv --motion <motn_id> --assigned-user me",
4770
5242
  " audienti prospects import-status <primp_id>",
4771
5243
  " audienti prospects show <prsp_id>",
4772
5244
  " audienti tools get email --url https://www.linkedin.com/in/example",
4773
5245
  "",
4774
5246
  "4. Find an existing prospect and inspect next step",
4775
5247
  " audienti prospects list --query \"name or company\" --wide",
5248
+ " audienti prospects list --assigned-user unassigned",
5249
+ " audienti prospects assign <prsp_id> --assigned-user me",
4776
5250
  " audienti companies search --query \"Honeywell\"",
4777
5251
  " audienti prospects list --company-profile <prof_id>",
4778
5252
  " audienti prospects show <prsp_id>",
@@ -4796,6 +5270,7 @@ const HELP_TOPICS = new Map([
4796
5270
  " audienti operator outcome <row_id> --payload <file.json>",
4797
5271
  "",
4798
5272
  "7. Inspect account analytics",
5273
+ " audienti users activity me --window 7d",
4799
5274
  " audienti analytics prospects --window 24h",
4800
5275
  " audienti analytics users --user me --window 30d",
4801
5276
  " audienti analytics visibility --window 24h --user me",