@audienti/cli 0.1.96 → 0.1.98
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 +20 -0
- package/README.md +44 -0
- package/package.json +1 -1
- package/skills/audienti/SKILL.md +15 -0
- package/src/api-client.js +46 -2
- package/src/cli.js +602 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to the Audienti CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.98] - 2026-10-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `content track <url>` submits an owned LinkedIn post for tracking. `content posts` lists tracked posts, and `content engagement <cpwi_id>` reads collected comments and reactions.
|
|
12
|
+
|
|
13
|
+
## [0.1.97] - 2026-10-04
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `tools email-find`, `tools linkedin-enrich`, `tools signals-find` and `tools write` run standalone account tools without creating prospects or experiments (#2554). Each returns a run id at once; `--wait` polls until the run finishes and prints the results, and `--request-key` makes a retry return the same run.
|
|
18
|
+
- `tools runs list|show|results|export` read tool runs after the CLI exits. Details expire after 90 days; the summary stays.
|
|
19
|
+
- `tools humanize --async` submits the humanizer as a tool run. Plain `tools humanize` is unchanged.
|
|
20
|
+
- `network list|export --cookie <scok_id>` reads the saved connections, followers and following of your own connected account, with coverage. It never visits LinkedIn or X.
|
|
21
|
+
- Requests now send `X-Audienti-Client: cli`, so runs show where they came from.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- API errors with HTTP 429 or 503 show the server's message.
|
|
26
|
+
|
|
7
27
|
## [0.1.96] - 2026-10-03
|
|
8
28
|
|
|
9
29
|
### Added
|
package/README.md
CHANGED
|
@@ -185,6 +185,9 @@ audienti motions update <motn_id> --payload motion-signals.json
|
|
|
185
185
|
audienti motions activate <motn_id>
|
|
186
186
|
audienti motions delete <motn_id> --confirm yes
|
|
187
187
|
audienti content programs
|
|
188
|
+
audienti content track <linkedin_post_url>
|
|
189
|
+
audienti content posts
|
|
190
|
+
audienti content engagement <cpwi_id> --json
|
|
188
191
|
audienti content plan <cprg_id>
|
|
189
192
|
audienti content approve <cpwi_id>
|
|
190
193
|
audienti content comments
|
|
@@ -231,6 +234,47 @@ audienti tools humanize --file draft.txt --tone professional > revised.txt
|
|
|
231
234
|
audienti tools humanize --file draft.txt --language English --json
|
|
232
235
|
```
|
|
233
236
|
|
|
237
|
+
### Standalone account tools
|
|
238
|
+
|
|
239
|
+
These run without creating prospects or experiments. Each command returns a run
|
|
240
|
+
id at once and keeps working on the server; `--wait` polls until the run
|
|
241
|
+
finishes and prints the results. If the CLI exits, read the run later with
|
|
242
|
+
`tools runs show|results`. Details expire after 90 days; the summary stays.
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
audienti tools email-find --first-name Ada --last-name Lovelace --company-domain example.com --wait
|
|
246
|
+
audienti tools email-find --file people.csv --request-key batch-2026-10-03 # csv, jsonl or json
|
|
247
|
+
audienti tools linkedin-enrich --url https://www.linkedin.com/in/example --wait --json
|
|
248
|
+
audienti tools signals-find --icp "Mid-size US logistics companies" --question "Who announced warehouse automation?" --count 5
|
|
249
|
+
audienti tools write --purpose "Book a call" --audience "Finance leads" --fact "Approvals take 2 days" --channel email
|
|
250
|
+
audienti tools humanize --file draft.txt --async --wait
|
|
251
|
+
audienti tools runs list --tool email-find
|
|
252
|
+
audienti tools runs results <trun_id> --json
|
|
253
|
+
audienti tools runs export <trun_id> --format csv --output results.csv
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`--file` takes JSON (an array or `{"items": [...]}`), JSONL, or CSV with a
|
|
257
|
+
header row. CSV is one row per line; quoted commas are allowed, line breaks
|
|
258
|
+
inside fields are not. With `--wait`, the exit code is 1 on an API error, a
|
|
259
|
+
timeout or an aborted (failed) run; per-item outcomes such as `no_match` do
|
|
260
|
+
not change it.
|
|
261
|
+
|
|
262
|
+
Prefer `tools email-find` for an email lookup. `tools get email` still runs the
|
|
263
|
+
prospect import pipeline and creates a prospect.
|
|
264
|
+
|
|
265
|
+
### Saved network
|
|
266
|
+
|
|
267
|
+
Your own connected account's saved connections and following, from what
|
|
268
|
+
normal work has already seen. Only the account's owner can read it, and it
|
|
269
|
+
never visits LinkedIn or X. Coverage says partial, not yet observed, or
|
|
270
|
+
capture not supported yet; none of these mean zero.
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
audienti network list --cookie <scok_id> --kind connection --query acme
|
|
274
|
+
audienti network list --cookie <scok_id> --kind follow --direction outgoing --json
|
|
275
|
+
audienti network export --cookie <scok_id> --kind connection --format csv --output connections.csv
|
|
276
|
+
```
|
|
277
|
+
|
|
234
278
|
To let an agent or operator check whether the local CLI is behind the latest
|
|
235
279
|
published package:
|
|
236
280
|
|
package/package.json
CHANGED
package/skills/audienti/SKILL.md
CHANGED
|
@@ -171,6 +171,14 @@ audienti analytics visibility --window 24h --user me --json
|
|
|
171
171
|
audienti analytics content --window week --json
|
|
172
172
|
audienti tools list --json
|
|
173
173
|
audienti tools humanize --file draft.txt --tone professional --json
|
|
174
|
+
audienti tools email-find --first-name Ada --last-name Lovelace --company-domain example.com --wait --json
|
|
175
|
+
audienti tools email-find --linkedin-url https://www.linkedin.com/in/example --wait --json
|
|
176
|
+
audienti tools linkedin-enrich --url https://www.linkedin.com/in/example --wait --json
|
|
177
|
+
audienti tools signals-find --icp "<ICP description>" --question "<signal question>" --wait --json
|
|
178
|
+
audienti tools write --purpose "<goal>" --audience "<who>" --fact "<fact>" --channel email --wait --json
|
|
179
|
+
audienti tools runs show <trun_id> --json
|
|
180
|
+
audienti tools runs results <trun_id> --json
|
|
181
|
+
audienti network list --cookie <scok_id> --kind connection --json
|
|
174
182
|
audienti tools linkedin-review --url https://www.linkedin.com/in/example --icp <icp_id> --json
|
|
175
183
|
audienti tools linkedin-review reports --json
|
|
176
184
|
audienti tools linkedin-review show <rprt_id> --json
|
|
@@ -281,3 +289,10 @@ drafting every message with `--mode report` or one selected row with
|
|
|
281
289
|
`--timeout-seconds <n>` for longer waits, or `--no-wait` to launch and return
|
|
282
290
|
immediately. Use `writer test-run show` with the report id to fetch the
|
|
283
291
|
completed report later.
|
|
292
|
+
|
|
293
|
+
## Standalone Tools Versus Prospect Tools
|
|
294
|
+
|
|
295
|
+
- For an email lookup with no prospect, use `audienti tools email-find`. `audienti tools get email` imports a prospect into the account; use it only when that is wanted.
|
|
296
|
+
- `tools email-find`, `linkedin-enrich`, `signals-find` and `write` return a run id immediately. Pass `--wait --json` to get results in one call, or poll `tools runs show <trun_id>` and read `tools runs results <trun_id>`. Nothing is sent and no prospects or experiments are created.
|
|
297
|
+
- Pass `--request-key <key>` when retrying a submission; the same key and input return the same run.
|
|
298
|
+
- `audienti network list|export` reads only the requester's own connected account's saved network. A 404 means the requester is not that account's owner. Coverage `unsupported_capture` or `not_yet_observed` is not zero.
|
package/src/api-client.js
CHANGED
|
@@ -699,6 +699,18 @@ export class AudientiClient {
|
|
|
699
699
|
return this.requestJson(accountPath(accountId, ["content_ops", "work_items", workItemId]));
|
|
700
700
|
}
|
|
701
701
|
|
|
702
|
+
contentTrackedPosts(accountId, query = {}) {
|
|
703
|
+
return this.requestJson(accountPath(accountId, ["content_ops", "tracked_posts"], query));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
contentTrackPost(accountId, body) {
|
|
707
|
+
return this.requestJson(accountPath(accountId, ["content_ops", "tracked_posts"]), { method: "POST", body });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
contentTrackedPost(accountId, workItemId) {
|
|
711
|
+
return this.requestJson(accountPath(accountId, ["content_ops", "tracked_posts", workItemId]));
|
|
712
|
+
}
|
|
713
|
+
|
|
702
714
|
contentFeedback(accountId, workItemId, body) {
|
|
703
715
|
return this.requestJson(accountPath(accountId, ["content_ops", "work_items", workItemId, "feedback"]), { method: "POST", body });
|
|
704
716
|
}
|
|
@@ -1195,6 +1207,37 @@ export class AudientiClient {
|
|
|
1195
1207
|
});
|
|
1196
1208
|
}
|
|
1197
1209
|
|
|
1210
|
+
// Standalone account tool runs (#2554).
|
|
1211
|
+
createToolRun(accountId, body) {
|
|
1212
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs"]), { method: "POST", body });
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
toolRuns(accountId, query = {}) {
|
|
1216
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs"], query));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
toolRun(accountId, runId) {
|
|
1220
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs", runId]));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
toolRunResults(accountId, runId, query = {}) {
|
|
1224
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs", runId, "results"], query));
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// CSV text by default; export_format=json returns an object.
|
|
1228
|
+
toolRunExport(accountId, runId, query = {}) {
|
|
1229
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs", runId, "export"], query));
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Saved network of one connected account (owner-only).
|
|
1233
|
+
socialCookieNetwork(accountId, socialCookieId, query = {}) {
|
|
1234
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "network"], query));
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
socialCookieNetworkExport(accountId, socialCookieId, query = {}) {
|
|
1238
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "network", "export"], query));
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1198
1241
|
humanizeText(accountId, body) {
|
|
1199
1242
|
return this.requestJson(accountPath(accountId, ["tools", "humanize"]), {
|
|
1200
1243
|
method: "POST",
|
|
@@ -1342,7 +1385,8 @@ export class AudientiClient {
|
|
|
1342
1385
|
|
|
1343
1386
|
headers(body) {
|
|
1344
1387
|
const headers = {
|
|
1345
|
-
Accept: "application/json"
|
|
1388
|
+
Accept: "application/json",
|
|
1389
|
+
"X-Audienti-Client": "cli"
|
|
1346
1390
|
};
|
|
1347
1391
|
|
|
1348
1392
|
if (this.token) {
|
|
@@ -1421,7 +1465,7 @@ function errorMessage(status, body) {
|
|
|
1421
1465
|
return details ? `Audienti rejected the request: ${details}` : "Audienti rejected the request.";
|
|
1422
1466
|
}
|
|
1423
1467
|
|
|
1424
|
-
if (status === 429 && body?.error) {
|
|
1468
|
+
if ((status === 429 || status === 503) && body?.error) {
|
|
1425
1469
|
return body.error;
|
|
1426
1470
|
}
|
|
1427
1471
|
|
package/src/cli.js
CHANGED
|
@@ -168,6 +168,9 @@ const MOTIONS_UPDATE_USAGE = "Usage: audienti motions update <motn_id> ([--statu
|
|
|
168
168
|
const CONTENT_PROGRAMS_USAGE = "Usage: audienti content programs [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
169
169
|
const CONTENT_PLAN_USAGE = "Usage: audienti content plan <cprg_id> [--week <n>] [--due] [--json] [--account <acct_id>]";
|
|
170
170
|
const CONTENT_SHOW_USAGE = "Usage: audienti content show <cpwi_id> [--json] [--account <acct_id>]";
|
|
171
|
+
const CONTENT_POSTS_USAGE = "Usage: audienti content posts [--user <account_user_id|email|name|me>] [--page <n>] [--json] [--account <acct_id>]";
|
|
172
|
+
const CONTENT_TRACK_USAGE = "Usage: audienti content track <linkedin_post_url> [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
173
|
+
const CONTENT_ENGAGEMENT_USAGE = "Usage: audienti content engagement <cpwi_id> [--json] [--account <acct_id>]";
|
|
171
174
|
const CONTENT_FEEDBACK_USAGE = "Usage: audienti content feedback <cpwi_id> (--message <text> | --payload <file.json>) [--json] [--account <acct_id>]";
|
|
172
175
|
const CONTENT_APPROVE_USAGE = "Usage: audienti content approve <cpwi_id> [--json] [--account <acct_id>]";
|
|
173
176
|
const CONTENT_SCHEDULE_USAGE = "Usage: audienti content schedule <cpwi_id> --at <time> [--json] [--account <acct_id>]";
|
|
@@ -214,7 +217,14 @@ const ANALYTICS_METRICS_USAGE = "Usage: audienti analytics metrics [--cohort-sta
|
|
|
214
217
|
const ANALYTICS_STAGES_USAGE = "Usage: audienti analytics stages [--interval weekly|monthly] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--list <list_id>] [--offer <offr_id>] [--icp <icp_id>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
215
218
|
const ANALYTICS_COHORT_LIST_USAGE = "Usage: audienti analytics cohorts create-list --name <text> --start YYYY-MM-DD --end YYYY-MM-DD [--event connection_request_sent] [--user <account_user_id|email|name|me>] [--note-mode <any|with_note|blank>] [--motion <motn_id>] [--offer <offr_id>] [--icp <icp_id>] [--play-tag <tag>] [--json] [--account <acct_id>]";
|
|
216
219
|
const TOOLS_LIST_USAGE = "Usage: audienti tools list [--json]";
|
|
217
|
-
const TOOLS_HUMANIZE_USAGE = "Usage: audienti tools humanize --file <path> [--tone <professional|academic|blog|casual|creative|scientific|technical>] [--language <language>] [--json] [--account <acct_id>]";
|
|
220
|
+
const TOOLS_HUMANIZE_USAGE = "Usage: audienti tools humanize --file <path> [--tone <professional|academic|blog|casual|creative|scientific|technical>] [--language <language>] [--async [--wait]] [--json] [--account <acct_id>]";
|
|
221
|
+
const TOOL_RUN_FLAGS_USAGE = "[--request-key <key>] [--wait [--timeout-seconds <n>] [--poll-interval-seconds <n>]] [--json] [--account <acct_id>]";
|
|
222
|
+
const TOOLS_EMAIL_FIND_USAGE = `Usage: audienti tools email-find (--first-name <name> --last-name <name> --company-domain <domain> | --linkedin-url <url> | --file <csv|jsonl|json>) ${TOOL_RUN_FLAGS_USAGE}`;
|
|
223
|
+
const TOOLS_LINKEDIN_ENRICH_USAGE = `Usage: audienti tools linkedin-enrich (--url <linkedin_url> [--kind <person|company>] | --file <csv|jsonl|json>) ${TOOL_RUN_FLAGS_USAGE}`;
|
|
224
|
+
const TOOLS_SIGNALS_FIND_USAGE = `Usage: audienti tools signals-find --icp <text> --question <text> [--count <1-10>] [--date-window-days <n>] ${TOOL_RUN_FLAGS_USAGE}`;
|
|
225
|
+
const TOOLS_WRITE_USAGE = `Usage: audienti tools write --purpose <text> --audience <text> --fact <text> [--fact <text> ...] --channel <channel> [--tone <tone>] [--subject|--no-subject] [--prospect <prsp_id>] ${TOOL_RUN_FLAGS_USAGE}`;
|
|
226
|
+
const TOOLS_RUNS_USAGE = "Usage: audienti tools runs (list [--tool <tool>] [--status <status>] [--limit <n>] [--cursor <id>] | show <trun_id> | results <trun_id> [--limit <n>] [--cursor <n>] | export <trun_id> [--format <csv|json>] [--output <path>]) [--json] [--account <acct_id>]";
|
|
227
|
+
const NETWORK_USAGE = "Usage: audienti network (list|export) --cookie <scok_id> [--platform <linkedin|twitter>] [--kind <connection|follow>] [--direction <incoming|outgoing>] [--query <text>] [--limit <n>] [--cursor <n>] [--format <csv|json>] [--output <path>] [--json] [--account <acct_id>]";
|
|
218
228
|
const TOOLS_LINKEDIN_REVIEW_USAGE = "Usage: audienti tools linkedin-review --url <linkedin_url> [--icp <icp_id>] [--json] [--account <acct_id>]";
|
|
219
229
|
const TOOLS_LINKEDIN_REVIEW_REPORTS_USAGE = "Usage: audienti tools linkedin-review reports [--limit <n>] [--json] [--account <acct_id>]";
|
|
220
230
|
const TOOLS_LINKEDIN_REVIEW_SHOW_USAGE = "Usage: audienti tools linkedin-review show <rprt_id> [--json] [--account <acct_id>]";
|
|
@@ -443,6 +453,9 @@ async function dispatch(argv, context) {
|
|
|
443
453
|
if (normalizedResource === "content" && action === "programs") return contentPrograms(rest, context, { accountOverride });
|
|
444
454
|
if (normalizedResource === "content" && action === "plan") return contentPlan(rest, context, { accountOverride });
|
|
445
455
|
if (normalizedResource === "content" && action === "show") return contentShow(rest, context, { accountOverride });
|
|
456
|
+
if (normalizedResource === "content" && action === "posts") return contentPosts(rest, context, { accountOverride });
|
|
457
|
+
if (normalizedResource === "content" && action === "track") return contentTrack(rest, context, { accountOverride });
|
|
458
|
+
if (normalizedResource === "content" && action === "engagement") return contentEngagement(rest, context, { accountOverride });
|
|
446
459
|
if (normalizedResource === "content" && action === "feedback") return contentFeedback(rest, context, { accountOverride });
|
|
447
460
|
if (normalizedResource === "content" && action === "approve") return contentApprove(rest, context, { accountOverride });
|
|
448
461
|
if (normalizedResource === "content" && action === "schedule") return contentSchedule(rest, context, { accountOverride });
|
|
@@ -480,6 +493,12 @@ async function dispatch(argv, context) {
|
|
|
480
493
|
if (normalizedResource === "tools" && action === "list") return toolsList(rest, context);
|
|
481
494
|
if (normalizedResource === "tools" && action === "get") return toolsGet(rest, context, { accountOverride });
|
|
482
495
|
if (normalizedResource === "tools" && action === "humanize") return toolsHumanize(rest, context, { accountOverride });
|
|
496
|
+
if (normalizedResource === "tools" && action === "email-find") return toolsEmailFind(rest, context, { accountOverride });
|
|
497
|
+
if (normalizedResource === "tools" && action === "linkedin-enrich") return toolsLinkedinEnrich(rest, context, { accountOverride });
|
|
498
|
+
if (normalizedResource === "tools" && action === "signals-find") return toolsSignalsFind(rest, context, { accountOverride });
|
|
499
|
+
if (normalizedResource === "tools" && action === "write") return toolsWrite(rest, context, { accountOverride });
|
|
500
|
+
if (normalizedResource === "tools" && action === "runs") return toolsRuns(rest, context, { accountOverride });
|
|
501
|
+
if (normalizedResource === "network") return networkCommand(action, rest, context, { accountOverride });
|
|
483
502
|
if (normalizedResource === "tools" && action === "linkedin-review") return toolsLinkedinReview(rest, context, { accountOverride });
|
|
484
503
|
if (normalizedResource === "tools" && action === "linkedin-strategy-review") return toolsLinkedinStrategyReview(rest, context, { accountOverride });
|
|
485
504
|
if (normalizedResource === "operator" && action === "failed-drafts") return operatorFailedDrafts(rest, context, { accountOverride });
|
|
@@ -3864,6 +3883,47 @@ async function contentShow(args, context, { accountOverride } = {}) {
|
|
|
3864
3883
|
renderContentWorkItem(item, context);
|
|
3865
3884
|
}
|
|
3866
3885
|
|
|
3886
|
+
async function contentPosts(args, context, { accountOverride } = {}) {
|
|
3887
|
+
const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), user: { type: "string" }, page: { type: "string" } });
|
|
3888
|
+
if (positionals.length) throw new CommandError(CONTENT_POSTS_USAGE);
|
|
3889
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
3890
|
+
const payload = await client.contentTrackedPosts(accountId, { ...(values.user ? { user: values.user } : {}), ...(values.page ? { page: values.page } : {}) });
|
|
3891
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
3892
|
+
for (const post of payload.posts || []) context.stdout.write(`${post.prefix_id} ${post.status} ${post.published_at || "Publication time unavailable"} ${post.url}\n`);
|
|
3893
|
+
if (payload.next_page) context.stdout.write(`More posts: content posts --page ${payload.next_page}\n`);
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
async function contentTrack(args, context, { accountOverride } = {}) {
|
|
3897
|
+
const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), user: { type: "string" } });
|
|
3898
|
+
if (positionals.length !== 1) throw new CommandError(CONTENT_TRACK_USAGE);
|
|
3899
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
3900
|
+
const post = await client.contentTrackPost(accountId, { url: positionals[0], ...(values.user ? { user: values.user } : {}) });
|
|
3901
|
+
if (values.json) return writeJson(context.stdout, post);
|
|
3902
|
+
context.stdout.write(`Tracking ${post.prefix_id}: ${post.status}\n${post.url}\n`);
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
async function contentEngagement(args, context, { accountOverride } = {}) {
|
|
3906
|
+
const { values, positionals } = parseCommandArgs(args, jsonOptions());
|
|
3907
|
+
if (positionals.length !== 1) throw new CommandError(CONTENT_ENGAGEMENT_USAGE);
|
|
3908
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
3909
|
+
const post = await client.contentTrackedPost(accountId, positionals[0]);
|
|
3910
|
+
if (values.json) return writeJson(context.stdout, post);
|
|
3911
|
+
context.stdout.write(`${post.title || "LinkedIn post"}: ${post.status}\n${post.url}\n`);
|
|
3912
|
+
if (post.error) context.stdout.write(`Tracking error: ${post.error}\n`);
|
|
3913
|
+
context.stdout.write(`Reposts reported: ${post.repost_count ?? "unavailable"}; reposter identities unavailable\n`);
|
|
3914
|
+
for (const category of ["comments", "reactions"]) {
|
|
3915
|
+
const state = post.categories?.[category] || {};
|
|
3916
|
+
context.stdout.write(`${category}: ${state.status || "pending"}, ${state.collected_count ?? 0} collected\n`);
|
|
3917
|
+
if (state.error) context.stdout.write(` ${state.error}\n`);
|
|
3918
|
+
for (const engagement of post[category] || []) {
|
|
3919
|
+
const person = engagement.profile || {};
|
|
3920
|
+
context.stdout.write(` ${person.name || "Unknown person"} | ${person.title || "Title unavailable"} | ${person.company || "Company unavailable"} | ${person.url || "Profile unavailable"}\n`);
|
|
3921
|
+
if (engagement.text) context.stdout.write(` ${engagement.commented_at || "Time unavailable"}: ${engagement.text}\n`);
|
|
3922
|
+
if (engagement.task?.id) context.stdout.write(` Reply task: ${engagement.task.id} (${engagement.task.delivery_state || engagement.task.status})\n`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3867
3927
|
async function contentFeedback(args, context, { accountOverride } = {}) {
|
|
3868
3928
|
const { values, positionals } = parseCommandArgs(args, {
|
|
3869
3929
|
...jsonOptions(),
|
|
@@ -4822,16 +4882,21 @@ async function toolsList(args, context) {
|
|
|
4822
4882
|
async function toolsHumanize(args, context, { accountOverride } = {}) {
|
|
4823
4883
|
const { values, positionals } = parseCommandArgs(args, {
|
|
4824
4884
|
...jsonOptions(),
|
|
4885
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
4825
4886
|
file: { type: "string" },
|
|
4826
4887
|
tone: { type: "string" },
|
|
4827
|
-
language: { type: "string" }
|
|
4888
|
+
language: { type: "string" },
|
|
4889
|
+
async: { type: "boolean" }
|
|
4828
4890
|
});
|
|
4829
4891
|
|
|
4830
|
-
if (positionals.length > 0 || !values.file) {
|
|
4892
|
+
if (positionals.length > 0 || !values.file || (values.wait && !values.async)) {
|
|
4831
4893
|
throw new CommandError(TOOLS_HUMANIZE_USAGE);
|
|
4832
4894
|
}
|
|
4833
4895
|
|
|
4834
4896
|
const text = await readHumanizerFile(values.file);
|
|
4897
|
+
if (values.async) {
|
|
4898
|
+
return submitToolRun("humanize", compactObject({ text, tone: values.tone, language: values.language }), values, context, { accountOverride });
|
|
4899
|
+
}
|
|
4835
4900
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
4836
4901
|
const payload = await client.humanizeText(accountId, {
|
|
4837
4902
|
text,
|
|
@@ -8562,6 +8627,41 @@ function availableTools() {
|
|
|
8562
8627
|
reports_command: null,
|
|
8563
8628
|
status_command: null
|
|
8564
8629
|
},
|
|
8630
|
+
{
|
|
8631
|
+
id: "email-find",
|
|
8632
|
+
command: "audienti tools email-find --first-name <name> --last-name <name> --company-domain <domain> | --linkedin-url <url> | --file <path>",
|
|
8633
|
+
description: "Find work emails without creating prospects. Returns a run id; add --wait for results.",
|
|
8634
|
+
reports_command: "audienti tools runs list --tool email-find",
|
|
8635
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8636
|
+
},
|
|
8637
|
+
{
|
|
8638
|
+
id: "linkedin-enrich",
|
|
8639
|
+
command: "audienti tools linkedin-enrich --url <linkedin_url> [--kind person|company] | --file <path>",
|
|
8640
|
+
description: "Enrich LinkedIn person or company URLs without creating prospects.",
|
|
8641
|
+
reports_command: "audienti tools runs list --tool linkedin-enrich",
|
|
8642
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8643
|
+
},
|
|
8644
|
+
{
|
|
8645
|
+
id: "signals-find",
|
|
8646
|
+
command: "audienti tools signals-find --icp <text> --question <text> [--count <n>]",
|
|
8647
|
+
description: "Find companies matching an ICP and a signal question, with source links.",
|
|
8648
|
+
reports_command: "audienti tools runs list --tool signals-find",
|
|
8649
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8650
|
+
},
|
|
8651
|
+
{
|
|
8652
|
+
id: "write",
|
|
8653
|
+
command: "audienti tools write --purpose <text> --audience <text> --fact <text> --channel <channel>",
|
|
8654
|
+
description: "Write one draft from a brief. Nothing is sent.",
|
|
8655
|
+
reports_command: "audienti tools runs list --tool write",
|
|
8656
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8657
|
+
},
|
|
8658
|
+
{
|
|
8659
|
+
id: "network",
|
|
8660
|
+
command: "audienti network list --cookie <scok_id> [--kind connection|follow]",
|
|
8661
|
+
description: "List or export your saved connections, followers and following (owner only).",
|
|
8662
|
+
reports_command: null,
|
|
8663
|
+
status_command: null
|
|
8664
|
+
},
|
|
8565
8665
|
{
|
|
8566
8666
|
id: "linkedin-review",
|
|
8567
8667
|
command: "audienti tools linkedin-review --url <linkedin_url> [--icp <icp_id>]",
|
|
@@ -10163,6 +10263,9 @@ const HELP_TOPICS = new Map([
|
|
|
10163
10263
|
" audienti content programs",
|
|
10164
10264
|
" audienti content plan <cprg_id>",
|
|
10165
10265
|
" audienti content show <cpwi_id>",
|
|
10266
|
+
" audienti content track <linkedin_post_url>",
|
|
10267
|
+
" audienti content posts",
|
|
10268
|
+
" audienti content engagement <cpwi_id>",
|
|
10166
10269
|
" audienti content feedback <cpwi_id> --message <text>",
|
|
10167
10270
|
" audienti content approve <cpwi_id>",
|
|
10168
10271
|
" audienti content publish <cpwi_id> --url <permalink>",
|
|
@@ -12505,6 +12608,9 @@ const HELP_TOPICS = new Map([
|
|
|
12505
12608
|
" audienti content programs [--user <account_user_id|email|name|me>] [--json]",
|
|
12506
12609
|
" audienti content plan <cprg_id> [--week <n>] [--due] [--json]",
|
|
12507
12610
|
" audienti content show <cpwi_id> [--json]",
|
|
12611
|
+
" audienti content track <linkedin_post_url> [--user <account_user_id|email|name|me>] [--json]",
|
|
12612
|
+
" audienti content posts [--user <account_user_id|email|name|me>] [--json]",
|
|
12613
|
+
" audienti content engagement <cpwi_id> [--json]",
|
|
12508
12614
|
" audienti content feedback <cpwi_id> --message <text> [--json]",
|
|
12509
12615
|
" audienti content approve <cpwi_id> [--json]",
|
|
12510
12616
|
" audienti content schedule <cpwi_id> --at <time> [--json]",
|
|
@@ -12520,6 +12626,9 @@ const HELP_TOPICS = new Map([
|
|
|
12520
12626
|
["content programs", [CONTENT_PROGRAMS_USAGE].join("\n")],
|
|
12521
12627
|
["content plan", [CONTENT_PLAN_USAGE].join("\n")],
|
|
12522
12628
|
["content show", [CONTENT_SHOW_USAGE].join("\n")],
|
|
12629
|
+
["content posts", [CONTENT_POSTS_USAGE].join("\n")],
|
|
12630
|
+
["content track", [CONTENT_TRACK_USAGE].join("\n")],
|
|
12631
|
+
["content engagement", [CONTENT_ENGAGEMENT_USAGE].join("\n")],
|
|
12523
12632
|
["content feedback", [CONTENT_FEEDBACK_USAGE].join("\n")],
|
|
12524
12633
|
["content approve", [CONTENT_APPROVE_USAGE].join("\n")],
|
|
12525
12634
|
["content schedule", [CONTENT_SCHEDULE_USAGE].join("\n")],
|
|
@@ -13828,7 +13937,13 @@ const HELP_TOPICS = new Map([
|
|
|
13828
13937
|
"Usage:",
|
|
13829
13938
|
" audienti tools list [--json]",
|
|
13830
13939
|
" audienti tools get <email|phone> --url <linkedin_url> [--json]",
|
|
13831
|
-
" audienti tools humanize --file <path> [--tone <tone>] [--language <language>] [--json]",
|
|
13940
|
+
" audienti tools humanize --file <path> [--tone <tone>] [--language <language>] [--async [--wait]] [--json]",
|
|
13941
|
+
" audienti tools email-find --first-name <name> --last-name <name> --company-domain <domain> [--wait] [--json]",
|
|
13942
|
+
" audienti tools email-find --linkedin-url <url> | --file <csv|jsonl|json> [--wait] [--json]",
|
|
13943
|
+
" audienti tools linkedin-enrich --url <linkedin_url> [--kind <person|company>] | --file <path> [--wait] [--json]",
|
|
13944
|
+
" audienti tools signals-find --icp <text> --question <text> [--count <n>] [--date-window-days <n>] [--wait] [--json]",
|
|
13945
|
+
" audienti tools write --purpose <text> --audience <text> --fact <text> --channel <channel> [--wait] [--json]",
|
|
13946
|
+
" audienti tools runs list|show <trun_id>|results <trun_id>|export <trun_id> [--json]",
|
|
13832
13947
|
" audienti tools linkedin-review --url <linkedin_url> [--icp <icp_id>] [--json]",
|
|
13833
13948
|
" audienti tools linkedin-review reports [--limit <n>] [--json]",
|
|
13834
13949
|
" audienti tools linkedin-review show <rprt_id> [--json]",
|
|
@@ -13844,6 +13959,11 @@ const HELP_TOPICS = new Map([
|
|
|
13844
13959
|
" audienti tools list Show available CLI tools and the report commands they support.",
|
|
13845
13960
|
" audienti tools get Run a LinkedIn URL through the existing import and contact-enrichment pipeline, then return the first selected email or phone.",
|
|
13846
13961
|
" audienti tools humanize Humanize arbitrary UTF-8 text and print the result.",
|
|
13962
|
+
" audienti tools email-find Find work emails without creating prospects (returns a run id).",
|
|
13963
|
+
" audienti tools linkedin-enrich Enrich LinkedIn person or company URLs without creating prospects.",
|
|
13964
|
+
" audienti tools signals-find Find companies matching an ICP and a signal question, with sources.",
|
|
13965
|
+
" audienti tools write Write one draft from a brief. Nothing is sent.",
|
|
13966
|
+
" audienti tools runs List tool runs and read their status, results and exports.",
|
|
13847
13967
|
" audienti tools linkedin-review Queue a LinkedIn personal profile authority review and ICP-fit positioning blueprint.",
|
|
13848
13968
|
" audienti tools linkedin-review reports List recent LinkedIn Review reports for the active account.",
|
|
13849
13969
|
" audienti tools linkedin-review show View the completed report content in the terminal.",
|
|
@@ -13908,12 +14028,15 @@ const HELP_TOPICS = new Map([
|
|
|
13908
14028
|
" --tone <professional|academic|blog|casual|creative|scientific|technical>",
|
|
13909
14029
|
" --language <name> Optional language hint, for example English",
|
|
13910
14030
|
"",
|
|
14031
|
+
" --async Submit as a tool run and print the run id instead (add --wait to poll for the result)",
|
|
14032
|
+
"",
|
|
13911
14033
|
"Output:",
|
|
13912
14034
|
" Plain text: only the humanized text, suitable for redirecting to a file",
|
|
13913
|
-
" JSON: { humanized_text, id, input_words }",
|
|
14035
|
+
" JSON: { humanized_text, id, input_words, run_id }",
|
|
13914
14036
|
"",
|
|
13915
14037
|
"API:",
|
|
13916
|
-
" POST /api/v1/accounts/:account_id/tools/humanize.json"
|
|
14038
|
+
" POST /api/v1/accounts/:account_id/tools/humanize.json",
|
|
14039
|
+
" POST /api/v1/accounts/:account_id/tools/runs.json (--async)"
|
|
13917
14040
|
].join("\n")],
|
|
13918
14041
|
|
|
13919
14042
|
["tools linkedin-review", [
|
|
@@ -15507,3 +15630,476 @@ async function runParityActionCommand(topic, args, context, { accountOverride }
|
|
|
15507
15630
|
|
|
15508
15631
|
writeLine(context.stdout, spec.done(positionals, payload));
|
|
15509
15632
|
}
|
|
15633
|
+
|
|
15634
|
+
// ---------------------------------------------------------------------------
|
|
15635
|
+
// Standalone account tools and saved network (#2554). Thin client of the
|
|
15636
|
+
// tools/runs and social_cookies/:id/network APIs: submit returns a run id
|
|
15637
|
+
// immediately; --wait polls the run, then prints its results.
|
|
15638
|
+
// ---------------------------------------------------------------------------
|
|
15639
|
+
|
|
15640
|
+
const TOOL_RUN_WAIT_OPTIONS = {
|
|
15641
|
+
"request-key": { type: "string" },
|
|
15642
|
+
wait: { type: "boolean" },
|
|
15643
|
+
"timeout-seconds": { type: "string" },
|
|
15644
|
+
"poll-interval-seconds": { type: "string" }
|
|
15645
|
+
};
|
|
15646
|
+
const DEFAULT_TOOL_RUN_TIMEOUT_SECONDS = 300;
|
|
15647
|
+
const DEFAULT_TOOL_RUN_POLL_INTERVAL_SECONDS = 2;
|
|
15648
|
+
const TOOL_RUN_FINISHED_STATUSES = ["completed", "failed"];
|
|
15649
|
+
|
|
15650
|
+
async function toolsEmailFind(args, context, { accountOverride } = {}) {
|
|
15651
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15652
|
+
...jsonOptions(),
|
|
15653
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15654
|
+
"first-name": { type: "string" },
|
|
15655
|
+
"last-name": { type: "string" },
|
|
15656
|
+
"company-domain": { type: "string" },
|
|
15657
|
+
"linkedin-url": { type: "string" },
|
|
15658
|
+
file: { type: "string" }
|
|
15659
|
+
});
|
|
15660
|
+
if (positionals.length > 0) throw new CommandError(TOOLS_EMAIL_FIND_USAGE);
|
|
15661
|
+
|
|
15662
|
+
const single = compactObject({
|
|
15663
|
+
first_name: values["first-name"],
|
|
15664
|
+
last_name: values["last-name"],
|
|
15665
|
+
company_domain: values["company-domain"],
|
|
15666
|
+
linkedin_url: values["linkedin-url"]
|
|
15667
|
+
});
|
|
15668
|
+
const items = values.file
|
|
15669
|
+
? await readToolItemsFile(values.file, ["client_item_id", "first_name", "last_name", "company_domain", "linkedin_url"])
|
|
15670
|
+
: [single];
|
|
15671
|
+
if (values.file && Object.keys(single).length > 0) throw new CommandError("Use either --file or the single-lookup flags, not both.");
|
|
15672
|
+
if (!values.file && !single.linkedin_url && !(single.first_name && single.last_name && single.company_domain)) {
|
|
15673
|
+
throw new CommandError(TOOLS_EMAIL_FIND_USAGE);
|
|
15674
|
+
}
|
|
15675
|
+
|
|
15676
|
+
return submitToolRun("email_find", { items }, values, context, { accountOverride });
|
|
15677
|
+
}
|
|
15678
|
+
|
|
15679
|
+
async function toolsLinkedinEnrich(args, context, { accountOverride } = {}) {
|
|
15680
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15681
|
+
...jsonOptions(),
|
|
15682
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15683
|
+
url: { type: "string" },
|
|
15684
|
+
kind: { type: "string" },
|
|
15685
|
+
file: { type: "string" }
|
|
15686
|
+
});
|
|
15687
|
+
if (positionals.length > 0 || (!values.url && !values.file) || (values.url && values.file)) {
|
|
15688
|
+
throw new CommandError(TOOLS_LINKEDIN_ENRICH_USAGE);
|
|
15689
|
+
}
|
|
15690
|
+
if (values.kind && !["person", "company"].includes(values.kind)) throw new CommandError("--kind must be person or company.");
|
|
15691
|
+
|
|
15692
|
+
const items = values.file
|
|
15693
|
+
? await readToolItemsFile(values.file, ["client_item_id", "linkedin_url", "kind"], { aliases: { url: "linkedin_url" } })
|
|
15694
|
+
: [compactObject({ linkedin_url: values.url, kind: values.kind })];
|
|
15695
|
+
return submitToolRun("linkedin_enrich", { items }, values, context, { accountOverride });
|
|
15696
|
+
}
|
|
15697
|
+
|
|
15698
|
+
async function toolsSignalsFind(args, context, { accountOverride } = {}) {
|
|
15699
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15700
|
+
...jsonOptions(),
|
|
15701
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15702
|
+
icp: { type: "string" },
|
|
15703
|
+
question: { type: "string" },
|
|
15704
|
+
count: { type: "string" },
|
|
15705
|
+
"date-window-days": { type: "string" }
|
|
15706
|
+
});
|
|
15707
|
+
if (positionals.length > 0 || !values.icp || !values.question) throw new CommandError(TOOLS_SIGNALS_FIND_USAGE);
|
|
15708
|
+
|
|
15709
|
+
const input = compactObject({
|
|
15710
|
+
icp_description: values.icp,
|
|
15711
|
+
question: values.question,
|
|
15712
|
+
count: normalizeOptionalPositiveInteger(values.count, "--count"),
|
|
15713
|
+
date_window_days: normalizeOptionalPositiveInteger(values["date-window-days"], "--date-window-days")
|
|
15714
|
+
});
|
|
15715
|
+
return submitToolRun("signals_find", input, values, context, { accountOverride });
|
|
15716
|
+
}
|
|
15717
|
+
|
|
15718
|
+
async function toolsWrite(args, context, { accountOverride } = {}) {
|
|
15719
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15720
|
+
...jsonOptions(),
|
|
15721
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15722
|
+
purpose: { type: "string" },
|
|
15723
|
+
audience: { type: "string" },
|
|
15724
|
+
fact: { type: "string", multiple: true },
|
|
15725
|
+
channel: { type: "string" },
|
|
15726
|
+
tone: { type: "string" },
|
|
15727
|
+
subject: { type: "boolean" },
|
|
15728
|
+
"no-subject": { type: "boolean" },
|
|
15729
|
+
prospect: { type: "string" }
|
|
15730
|
+
});
|
|
15731
|
+
const facts = (values.fact || []).map((fact) => fact.trim()).filter(Boolean);
|
|
15732
|
+
if (positionals.length > 0 || !values.purpose || !values.audience || !values.channel || facts.length === 0) {
|
|
15733
|
+
throw new CommandError(TOOLS_WRITE_USAGE);
|
|
15734
|
+
}
|
|
15735
|
+
if (values.subject && values["no-subject"]) throw new CommandError("Choose either --subject or --no-subject.");
|
|
15736
|
+
|
|
15737
|
+
const input = compactObject({
|
|
15738
|
+
purpose: values.purpose,
|
|
15739
|
+
audience: values.audience,
|
|
15740
|
+
facts,
|
|
15741
|
+
channel: values.channel,
|
|
15742
|
+
tone: values.tone,
|
|
15743
|
+
prospect_id: values.prospect,
|
|
15744
|
+
subject: values.subject ? true : (values["no-subject"] ? false : undefined)
|
|
15745
|
+
});
|
|
15746
|
+
return submitToolRun("write", input, values, context, { accountOverride });
|
|
15747
|
+
}
|
|
15748
|
+
|
|
15749
|
+
async function submitToolRun(tool, input, values, context, { accountOverride } = {}) {
|
|
15750
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15751
|
+
const run = await client.createToolRun(accountId, compactObject({ tool, input, request_key: values["request-key"] }));
|
|
15752
|
+
if (!values.wait) {
|
|
15753
|
+
if (values.json) return writeJson(context.stdout, run);
|
|
15754
|
+
|
|
15755
|
+
writeLine(context.stdout, `${run?.replayed ? "Existing" : "Queued"} run ${display(run?.id)} (${tool}, ${display(run?.item_count, 0)} item(s)).`);
|
|
15756
|
+
writeLine(context.stdout, `Check it with: audienti tools runs show ${display(run?.id)}`);
|
|
15757
|
+
return 0;
|
|
15758
|
+
}
|
|
15759
|
+
|
|
15760
|
+
return finishWaitedRun(client, accountId, run, values, context);
|
|
15761
|
+
}
|
|
15762
|
+
|
|
15763
|
+
async function finishWaitedRun(client, accountId, run, values, context) {
|
|
15764
|
+
const finished = await waitForToolRun(client, accountId, run, {
|
|
15765
|
+
timeoutSeconds: normalizeOptionalPositiveInteger(values["timeout-seconds"], "--timeout-seconds") || DEFAULT_TOOL_RUN_TIMEOUT_SECONDS,
|
|
15766
|
+
pollIntervalSeconds: normalizeOptionalPositiveInteger(values["poll-interval-seconds"], "--poll-interval-seconds") || DEFAULT_TOOL_RUN_POLL_INTERVAL_SECONDS,
|
|
15767
|
+
sleepImpl: context.sleep
|
|
15768
|
+
});
|
|
15769
|
+
const items = await fetchAllToolRunItems(client, accountId, finished.id);
|
|
15770
|
+
if (values.json) {
|
|
15771
|
+
writeJson(context.stdout, { run: finished, items });
|
|
15772
|
+
} else {
|
|
15773
|
+
renderToolRun(finished, context);
|
|
15774
|
+
renderToolRunItems(finished.tool, items, context);
|
|
15775
|
+
}
|
|
15776
|
+
return finished.status === "failed" ? 1 : 0;
|
|
15777
|
+
}
|
|
15778
|
+
|
|
15779
|
+
async function waitForToolRun(client, accountId, run, { timeoutSeconds, pollIntervalSeconds, sleepImpl = sleep }) {
|
|
15780
|
+
if (!run?.id) throw new CommandError("The tool run response did not include a run id.");
|
|
15781
|
+
if (TOOL_RUN_FINISHED_STATUSES.includes(run.status)) return run;
|
|
15782
|
+
|
|
15783
|
+
const timeoutAt = Date.now() + (timeoutSeconds * 1000);
|
|
15784
|
+
let latest = run;
|
|
15785
|
+
while (Date.now() < timeoutAt) {
|
|
15786
|
+
await sleepImpl(pollIntervalSeconds * 1000);
|
|
15787
|
+
latest = await client.toolRun(accountId, run.id);
|
|
15788
|
+
if (TOOL_RUN_FINISHED_STATUSES.includes(latest?.status)) return latest;
|
|
15789
|
+
}
|
|
15790
|
+
|
|
15791
|
+
throw new CommandError(`Timed out after ${timeoutSeconds} seconds waiting for run ${run.id}. It keeps running; check it with: audienti tools runs show ${run.id}`);
|
|
15792
|
+
}
|
|
15793
|
+
|
|
15794
|
+
async function fetchAllToolRunItems(client, accountId, runId) {
|
|
15795
|
+
const items = [];
|
|
15796
|
+
let cursor;
|
|
15797
|
+
for (let page = 0; page < 100; page += 1) {
|
|
15798
|
+
const payload = await client.toolRunResults(accountId, runId, compactObject({ limit: 200, cursor }));
|
|
15799
|
+
items.push(...(Array.isArray(payload?.items) ? payload.items : []));
|
|
15800
|
+
cursor = payload?.next_cursor;
|
|
15801
|
+
if (!cursor) break;
|
|
15802
|
+
}
|
|
15803
|
+
return items;
|
|
15804
|
+
}
|
|
15805
|
+
|
|
15806
|
+
async function toolsRuns(args, context, { accountOverride } = {}) {
|
|
15807
|
+
const [subcommand, ...rest] = args;
|
|
15808
|
+
if (subcommand === "list") return toolsRunsList(rest, context, { accountOverride });
|
|
15809
|
+
if (subcommand === "show") return toolsRunsShow(rest, context, { accountOverride });
|
|
15810
|
+
if (subcommand === "results") return toolsRunsResults(rest, context, { accountOverride });
|
|
15811
|
+
if (subcommand === "export") return toolsRunsExport(rest, context, { accountOverride });
|
|
15812
|
+
|
|
15813
|
+
throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15814
|
+
}
|
|
15815
|
+
|
|
15816
|
+
async function toolsRunsList(args, context, { accountOverride } = {}) {
|
|
15817
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15818
|
+
...jsonOptions(),
|
|
15819
|
+
tool: { type: "string" },
|
|
15820
|
+
status: { type: "string" },
|
|
15821
|
+
limit: { type: "string" },
|
|
15822
|
+
cursor: { type: "string" }
|
|
15823
|
+
});
|
|
15824
|
+
if (positionals.length > 0) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15825
|
+
|
|
15826
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15827
|
+
const payload = await client.toolRuns(accountId, compactObject({
|
|
15828
|
+
tool: values.tool ? values.tool.replaceAll("-", "_") : undefined,
|
|
15829
|
+
status: values.status,
|
|
15830
|
+
limit: normalizeOptionalPositiveInteger(values.limit, "--limit"),
|
|
15831
|
+
cursor: values.cursor
|
|
15832
|
+
}));
|
|
15833
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
15834
|
+
|
|
15835
|
+
const runs = Array.isArray(payload?.runs) ? payload.runs : [];
|
|
15836
|
+
if (runs.length === 0) return writeLine(context.stdout, "No tool runs.");
|
|
15837
|
+
writeAlignedTable(context, ["ID", "TOOL", "STATUS", "ITEMS", "CREATED"],
|
|
15838
|
+
runs.map((run) => [run.id, run.tool, run.details_expired ? `${run.status} (details expired)` : run.status, run.item_count, run.created_at]));
|
|
15839
|
+
if (payload?.next_cursor) writeLine(context.stdout, `More runs: audienti tools runs list --cursor ${payload.next_cursor}`);
|
|
15840
|
+
}
|
|
15841
|
+
|
|
15842
|
+
async function toolsRunsShow(args, context, { accountOverride } = {}) {
|
|
15843
|
+
const { values, positionals } = parseCommandArgs(args, jsonOptions());
|
|
15844
|
+
if (positionals.length !== 1) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15845
|
+
|
|
15846
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15847
|
+
const run = await client.toolRun(accountId, positionals[0]);
|
|
15848
|
+
if (values.json) return writeJson(context.stdout, run);
|
|
15849
|
+
|
|
15850
|
+
renderToolRun(run, context);
|
|
15851
|
+
return run?.status === "failed" ? 1 : 0;
|
|
15852
|
+
}
|
|
15853
|
+
|
|
15854
|
+
async function toolsRunsResults(args, context, { accountOverride } = {}) {
|
|
15855
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15856
|
+
...jsonOptions(),
|
|
15857
|
+
limit: { type: "string" },
|
|
15858
|
+
cursor: { type: "string" }
|
|
15859
|
+
});
|
|
15860
|
+
if (positionals.length !== 1) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15861
|
+
|
|
15862
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15863
|
+
const payload = await client.toolRunResults(accountId, positionals[0], compactObject({
|
|
15864
|
+
limit: normalizeOptionalPositiveInteger(values.limit, "--limit"),
|
|
15865
|
+
cursor: values.cursor
|
|
15866
|
+
}));
|
|
15867
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
15868
|
+
|
|
15869
|
+
if (payload?.details_expired) return writeLine(context.stdout, "This run's details expired after 90 days; only its summary remains.");
|
|
15870
|
+
renderToolRunItems(payload?.run?.tool, Array.isArray(payload?.items) ? payload.items : [], context);
|
|
15871
|
+
if (payload?.next_cursor) writeLine(context.stdout, `More results: audienti tools runs results ${positionals[0]} --cursor ${payload.next_cursor}`);
|
|
15872
|
+
}
|
|
15873
|
+
|
|
15874
|
+
async function toolsRunsExport(args, context, { accountOverride } = {}) {
|
|
15875
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15876
|
+
format: { type: "string" },
|
|
15877
|
+
output: { type: "string" }
|
|
15878
|
+
});
|
|
15879
|
+
if (positionals.length !== 1) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15880
|
+
const format = exportFormat(values.format);
|
|
15881
|
+
|
|
15882
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15883
|
+
const payload = await client.toolRunExport(accountId, positionals[0], format === "json" ? { export_format: "json" } : {});
|
|
15884
|
+
return writeExportPayload(payload, format, values.output, context);
|
|
15885
|
+
}
|
|
15886
|
+
|
|
15887
|
+
async function networkCommand(action, args, context, { accountOverride } = {}) {
|
|
15888
|
+
if (!["list", "export"].includes(action)) throw new CommandError(NETWORK_USAGE);
|
|
15889
|
+
|
|
15890
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15891
|
+
...(action === "list" ? jsonOptions() : {}),
|
|
15892
|
+
cookie: { type: "string" },
|
|
15893
|
+
platform: { type: "string" },
|
|
15894
|
+
kind: { type: "string" },
|
|
15895
|
+
direction: { type: "string" },
|
|
15896
|
+
query: { type: "string" },
|
|
15897
|
+
...(action === "list" ? { limit: { type: "string" }, cursor: { type: "string" } } : { format: { type: "string" }, output: { type: "string" } })
|
|
15898
|
+
});
|
|
15899
|
+
if (positionals.length > 0 || !values.cookie) throw new CommandError(NETWORK_USAGE);
|
|
15900
|
+
|
|
15901
|
+
const query = compactObject({
|
|
15902
|
+
platform: values.platform || "linkedin",
|
|
15903
|
+
kind: values.kind || "connection",
|
|
15904
|
+
direction: values.direction,
|
|
15905
|
+
network_query: values.query
|
|
15906
|
+
});
|
|
15907
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15908
|
+
|
|
15909
|
+
if (action === "export") {
|
|
15910
|
+
const format = exportFormat(values.format);
|
|
15911
|
+
const payload = await client.socialCookieNetworkExport(accountId, values.cookie, format === "json" ? { ...query, export_format: "json" } : query);
|
|
15912
|
+
return writeExportPayload(payload, format, values.output, context);
|
|
15913
|
+
}
|
|
15914
|
+
|
|
15915
|
+
const payload = await client.socialCookieNetwork(accountId, values.cookie, compactObject({
|
|
15916
|
+
...query,
|
|
15917
|
+
limit: normalizeOptionalPositiveInteger(values.limit, "--limit"),
|
|
15918
|
+
cursor: values.cursor
|
|
15919
|
+
}));
|
|
15920
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
15921
|
+
|
|
15922
|
+
renderNetworkCoverage(payload?.coverage, context);
|
|
15923
|
+
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
|
|
15924
|
+
if (rows.length > 0) {
|
|
15925
|
+
writeAlignedTable(context, ["NAME", "USERNAME", "ID", "LAST SEEN"],
|
|
15926
|
+
rows.map((row) => [row.display_name, row.alias_key, row.stable_id, row.last_observed_at]));
|
|
15927
|
+
}
|
|
15928
|
+
if (payload?.next_cursor) writeLine(context.stdout, `More rows: audienti network list --cookie ${values.cookie} --cursor ${payload.next_cursor}`);
|
|
15929
|
+
}
|
|
15930
|
+
|
|
15931
|
+
function renderNetworkCoverage(coverage, context) {
|
|
15932
|
+
if (!coverage) return;
|
|
15933
|
+
|
|
15934
|
+
const state = {
|
|
15935
|
+
unsupported_capture: "capture not supported yet (not zero)",
|
|
15936
|
+
not_yet_observed: "not yet observed",
|
|
15937
|
+
partial: "partial (only what normal work has seen)",
|
|
15938
|
+
complete: "complete"
|
|
15939
|
+
}[coverage.state] || display(coverage.state);
|
|
15940
|
+
writeLine(context.stdout, `${display(coverage.platform)} ${display(coverage.kind)} ${display(coverage.direction)}: ${state}` +
|
|
15941
|
+
(coverage.last_observed_at ? `, last seen ${coverage.last_observed_at}` : ""));
|
|
15942
|
+
}
|
|
15943
|
+
|
|
15944
|
+
function exportFormat(value) {
|
|
15945
|
+
const format = String(value || "csv").toLowerCase();
|
|
15946
|
+
if (!["csv", "json"].includes(format)) throw new CommandError("--format must be csv or json.");
|
|
15947
|
+
return format;
|
|
15948
|
+
}
|
|
15949
|
+
|
|
15950
|
+
async function writeExportPayload(payload, format, outputPath, context) {
|
|
15951
|
+
const text = format === "json" ? `${JSON.stringify(payload, null, 2)}\n` : String(payload ?? "");
|
|
15952
|
+
if (outputPath) {
|
|
15953
|
+
await writeFile(outputPath, text);
|
|
15954
|
+
return writeLine(context.stderr, `Wrote ${Buffer.byteLength(text)} bytes to ${outputPath}`);
|
|
15955
|
+
}
|
|
15956
|
+
context.stdout.write(text);
|
|
15957
|
+
}
|
|
15958
|
+
|
|
15959
|
+
function renderToolRun(run, context) {
|
|
15960
|
+
writeLine(context.stdout, `Run ${display(run?.id)} (${display(run?.tool)}): ${display(run?.status)}`);
|
|
15961
|
+
writeLine(context.stdout, `Items: ${display(run?.item_count, 0)} total, ${display(run?.completed_item_count, 0)} completed, ${display(run?.failed_item_count, 0)} failed`);
|
|
15962
|
+
const outcomes = run?.summary?.outcomes || {};
|
|
15963
|
+
if (Object.keys(outcomes).length > 0) {
|
|
15964
|
+
writeLine(context.stdout, `Outcomes: ${Object.entries(outcomes).map(([outcome, count]) => `${outcome} ${count}`).join(", ")}`);
|
|
15965
|
+
}
|
|
15966
|
+
if (run?.error_code) writeLine(context.stdout, `Error: ${run.error_code}`);
|
|
15967
|
+
if (run?.details_expired) writeLine(context.stdout, "Details expired after 90 days; only this summary remains.");
|
|
15968
|
+
}
|
|
15969
|
+
|
|
15970
|
+
function renderToolRunItems(tool, items, context) {
|
|
15971
|
+
if (items.length === 0) return writeLine(context.stdout, "No results yet.");
|
|
15972
|
+
|
|
15973
|
+
writeAlignedTable(context, ["#", "ITEM", "OUTCOME", "RESULT"],
|
|
15974
|
+
items.map((item) => [item.position, item.client_item_id, item.outcome || item.status, toolItemSummary(tool, item)]));
|
|
15975
|
+
}
|
|
15976
|
+
|
|
15977
|
+
function toolItemSummary(tool, item) {
|
|
15978
|
+
const result = item?.result || {};
|
|
15979
|
+
if (item?.error_message && item.outcome !== "success") return item.error_message;
|
|
15980
|
+
if (tool === "email_find") return result.email || "";
|
|
15981
|
+
if (tool === "linkedin_enrich") return [result.display_name || result.name, result.headline || result.domain].filter(Boolean).join(" - ");
|
|
15982
|
+
if (tool === "signals_find") return (result.companies || []).map((company) => company.name).join(", ");
|
|
15983
|
+
if (tool === "write") return singleLine([result.subject, result.text].filter(Boolean).join(" | "));
|
|
15984
|
+
if (tool === "humanize") return singleLine(result.humanized_text);
|
|
15985
|
+
if (tool === "network_export") return `${display(result.exported_row_count, 0)} row(s)`;
|
|
15986
|
+
return Object.keys(result).length > 0 ? JSON.stringify(result) : "";
|
|
15987
|
+
}
|
|
15988
|
+
|
|
15989
|
+
// Reads tool items from CSV (header row), JSONL, a JSON array or {items: [...]}.
|
|
15990
|
+
async function readToolItemsFile(filePath, fields, { aliases = {} } = {}) {
|
|
15991
|
+
let contents;
|
|
15992
|
+
try {
|
|
15993
|
+
contents = await readFile(filePath, "utf8");
|
|
15994
|
+
} catch (error) {
|
|
15995
|
+
throw new CommandError(`Could not read items file ${filePath}: ${error.message}`);
|
|
15996
|
+
}
|
|
15997
|
+
const trimmed = String(contents || "").trim();
|
|
15998
|
+
if (!trimmed) throw new CommandError(`Items file ${filePath} is empty.`);
|
|
15999
|
+
|
|
16000
|
+
let rows;
|
|
16001
|
+
let whole;
|
|
16002
|
+
try {
|
|
16003
|
+
whole = JSON.parse(trimmed);
|
|
16004
|
+
} catch {
|
|
16005
|
+
whole = undefined;
|
|
16006
|
+
}
|
|
16007
|
+
if (whole !== undefined) {
|
|
16008
|
+
rows = Array.isArray(whole) ? whole : (Array.isArray(whole?.items) ? whole.items : [whole]);
|
|
16009
|
+
} else {
|
|
16010
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
16011
|
+
if (lines[0].startsWith("{") || lines[0].startsWith("[")) {
|
|
16012
|
+
rows = lines.map((line, index) => {
|
|
16013
|
+
try {
|
|
16014
|
+
return JSON.parse(line);
|
|
16015
|
+
} catch (error) {
|
|
16016
|
+
throw new CommandError(`Invalid JSON or JSONL row ${index + 1} in ${filePath}: ${error.message}`);
|
|
16017
|
+
}
|
|
16018
|
+
});
|
|
16019
|
+
} else {
|
|
16020
|
+
// CSV: one row per line; quoted commas allowed, no line breaks inside fields.
|
|
16021
|
+
const headers = parseCsvLine(lines[0]).map((header) => header.trim());
|
|
16022
|
+
rows = lines.slice(1).map((line) => {
|
|
16023
|
+
const values = parseCsvLine(line);
|
|
16024
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
|
|
16025
|
+
});
|
|
16026
|
+
}
|
|
16027
|
+
}
|
|
16028
|
+
|
|
16029
|
+
return rows.map((row) => {
|
|
16030
|
+
const normalized = {};
|
|
16031
|
+
for (const [rawKey, value] of Object.entries(row || {})) {
|
|
16032
|
+
const key = rawKey.trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
|
|
16033
|
+
const field = aliases[key] || key;
|
|
16034
|
+
if (fields.includes(field) && value !== undefined && value !== null) normalized[field] = String(value);
|
|
16035
|
+
}
|
|
16036
|
+
return compactObject(normalized);
|
|
16037
|
+
});
|
|
16038
|
+
}
|
|
16039
|
+
|
|
16040
|
+
const TOOL_RUN_HELP_FOOTER = [
|
|
16041
|
+
"",
|
|
16042
|
+
"Behavior:",
|
|
16043
|
+
" Returns a run id immediately; the work runs on the server. Nothing is sent and no prospects are created.",
|
|
16044
|
+
" --wait polls `audienti tools runs show` until the run finishes, then prints its results.",
|
|
16045
|
+
" If the CLI exits early, read the run later with `audienti tools runs show|results <trun_id>`.",
|
|
16046
|
+
" --request-key makes a retry safe: the same key and input return the same run.",
|
|
16047
|
+
"",
|
|
16048
|
+
"Files (--file):",
|
|
16049
|
+
" JSON (an array or {\"items\": [...]}, any formatting), JSONL (one object per line) or CSV with a header row.",
|
|
16050
|
+
" CSV is one row per line; quoted commas are allowed, line breaks inside fields are not.",
|
|
16051
|
+
"",
|
|
16052
|
+
"Exit codes:",
|
|
16053
|
+
" 1 on an API error, a --wait timeout or an aborted (failed) run; otherwise 0.",
|
|
16054
|
+
" Per-item outcomes such as no_match or invalid_input do not change the exit code; read them in the results.",
|
|
16055
|
+
"",
|
|
16056
|
+
"API:",
|
|
16057
|
+
" POST /api/v1/accounts/:account_id/tools/runs.json",
|
|
16058
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id.json",
|
|
16059
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id/results.json"
|
|
16060
|
+
];
|
|
16061
|
+
|
|
16062
|
+
for (const [topic, usage, purpose] of [
|
|
16063
|
+
["tools email-find", TOOLS_EMAIL_FIND_USAGE, "Finds work emails from a name and company domain, or from a LinkedIn URL. Prefer this over `tools get email` when you do not want a prospect created."],
|
|
16064
|
+
["tools linkedin-enrich", TOOLS_LINKEDIN_ENRICH_USAGE, "Reads public LinkedIn person or company details through API providers. Never uses your connected LinkedIn account."],
|
|
16065
|
+
["tools signals-find", TOOLS_SIGNALS_FIND_USAGE, "Finds companies that match an ICP description and a signal question, each with a source link. Returns fewer companies, or no match, when proof is weak."],
|
|
16066
|
+
["tools write", TOOLS_WRITE_USAGE, "Writes one draft from your brief using only the facts you give. --prospect adds the existing prospect's name, title and company as read-only context."]
|
|
16067
|
+
]) {
|
|
16068
|
+
HELP_TOPICS.set(topic, ["Usage:", ` ${usage.slice("Usage: ".length)}`, "", "Status: implemented", "", "Purpose:", ` ${purpose}`, ...TOOL_RUN_HELP_FOOTER].join("\n"));
|
|
16069
|
+
}
|
|
16070
|
+
|
|
16071
|
+
HELP_TOPICS.set("tools runs", [
|
|
16072
|
+
"Usage:",
|
|
16073
|
+
` ${TOOLS_RUNS_USAGE.slice("Usage: ".length)}`,
|
|
16074
|
+
"",
|
|
16075
|
+
"Status: implemented",
|
|
16076
|
+
"",
|
|
16077
|
+
"Purpose:",
|
|
16078
|
+
" Lists tool runs and reads one run's status, per-item results and export. Details expire after 90 days; the summary remains.",
|
|
16079
|
+
" Saved-network runs show their results only to the connected account's owner.",
|
|
16080
|
+
"",
|
|
16081
|
+
"API:",
|
|
16082
|
+
" GET /api/v1/accounts/:account_id/tools/runs.json",
|
|
16083
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id.json",
|
|
16084
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id/results.json",
|
|
16085
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id/export.json"
|
|
16086
|
+
].join("\n"));
|
|
16087
|
+
|
|
16088
|
+
HELP_TOPICS.set("network", [
|
|
16089
|
+
"Usage:",
|
|
16090
|
+
` ${NETWORK_USAGE.slice("Usage: ".length)}`,
|
|
16091
|
+
"",
|
|
16092
|
+
"Status: implemented",
|
|
16093
|
+
"",
|
|
16094
|
+
"Purpose:",
|
|
16095
|
+
" Lists, searches or exports the saved connections, followers and following of one connected account.",
|
|
16096
|
+
" Only that account's owner can read it. It shows what normal work has already seen; it never visits LinkedIn or X.",
|
|
16097
|
+
" Coverage says partial, not yet observed, or capture not supported yet; none of these mean zero.",
|
|
16098
|
+
"",
|
|
16099
|
+
"Defaults:",
|
|
16100
|
+
" --platform linkedin --kind connection (follows need --direction incoming|outgoing)",
|
|
16101
|
+
"",
|
|
16102
|
+
"API:",
|
|
16103
|
+
" GET /api/v1/accounts/:account_id/social_cookies/:social_cookie_id/network.json",
|
|
16104
|
+
" GET /api/v1/accounts/:account_id/social_cookies/:social_cookie_id/network/export.json"
|
|
16105
|
+
].join("\n"));
|