@audienti/cli 0.1.95 → 0.1.97
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 +22 -0
- package/README.md +41 -0
- package/package.json +1 -1
- package/skills/audienti/SKILL.md +15 -0
- package/src/api-client.js +34 -2
- package/src/cli.js +546 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to the Audienti CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.97] - 2026-10-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `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.
|
|
12
|
+
- `tools runs list|show|results|export` read tool runs after the CLI exits. Details expire after 90 days; the summary stays.
|
|
13
|
+
- `tools humanize --async` submits the humanizer as a tool run. Plain `tools humanize` is unchanged.
|
|
14
|
+
- `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.
|
|
15
|
+
- Requests now send `X-Audienti-Client: cli`, so runs show where they came from.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- API errors with HTTP 429 or 503 show the server's message.
|
|
20
|
+
|
|
21
|
+
## [0.1.96] - 2026-10-03
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `PATCH /api/v1/accounts/:account_id/quick_start/:id` changes one card of a ready setup draft (`section` is `icp`, `offer` or `signals`) and returns the same JSON as the draft read. New job titles bring new example people in the response (#2551).
|
|
26
|
+
- The draft preview now includes `icp.company_sizes`, `icp.company_types` and `signals`.
|
|
27
|
+
- Reading a ready draft whose job titles were changed on the setup page makes its example people first, so `example_people` is never left empty by an edit.
|
|
28
|
+
|
|
7
29
|
## [0.1.95] - 2026-10-03
|
|
8
30
|
|
|
9
31
|
### Changed
|
package/README.md
CHANGED
|
@@ -231,6 +231,47 @@ audienti tools humanize --file draft.txt --tone professional > revised.txt
|
|
|
231
231
|
audienti tools humanize --file draft.txt --language English --json
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
+
### Standalone account tools
|
|
235
|
+
|
|
236
|
+
These run without creating prospects or experiments. Each command returns a run
|
|
237
|
+
id at once and keeps working on the server; `--wait` polls until the run
|
|
238
|
+
finishes and prints the results. If the CLI exits, read the run later with
|
|
239
|
+
`tools runs show|results`. Details expire after 90 days; the summary stays.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
audienti tools email-find --first-name Ada --last-name Lovelace --company-domain example.com --wait
|
|
243
|
+
audienti tools email-find --file people.csv --request-key batch-2026-10-03 # csv, jsonl or json
|
|
244
|
+
audienti tools linkedin-enrich --url https://www.linkedin.com/in/example --wait --json
|
|
245
|
+
audienti tools signals-find --icp "Mid-size US logistics companies" --question "Who announced warehouse automation?" --count 5
|
|
246
|
+
audienti tools write --purpose "Book a call" --audience "Finance leads" --fact "Approvals take 2 days" --channel email
|
|
247
|
+
audienti tools humanize --file draft.txt --async --wait
|
|
248
|
+
audienti tools runs list --tool email-find
|
|
249
|
+
audienti tools runs results <trun_id> --json
|
|
250
|
+
audienti tools runs export <trun_id> --format csv --output results.csv
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`--file` takes JSON (an array or `{"items": [...]}`), JSONL, or CSV with a
|
|
254
|
+
header row. CSV is one row per line; quoted commas are allowed, line breaks
|
|
255
|
+
inside fields are not. With `--wait`, the exit code is 1 on an API error, a
|
|
256
|
+
timeout or an aborted (failed) run; per-item outcomes such as `no_match` do
|
|
257
|
+
not change it.
|
|
258
|
+
|
|
259
|
+
Prefer `tools email-find` for an email lookup. `tools get email` still runs the
|
|
260
|
+
prospect import pipeline and creates a prospect.
|
|
261
|
+
|
|
262
|
+
### Saved network
|
|
263
|
+
|
|
264
|
+
Your own connected account's saved connections and following, from what
|
|
265
|
+
normal work has already seen. Only the account's owner can read it, and it
|
|
266
|
+
never visits LinkedIn or X. Coverage says partial, not yet observed, or
|
|
267
|
+
capture not supported yet; none of these mean zero.
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
audienti network list --cookie <scok_id> --kind connection --query acme
|
|
271
|
+
audienti network list --cookie <scok_id> --kind follow --direction outgoing --json
|
|
272
|
+
audienti network export --cookie <scok_id> --kind connection --format csv --output connections.csv
|
|
273
|
+
```
|
|
274
|
+
|
|
234
275
|
To let an agent or operator check whether the local CLI is behind the latest
|
|
235
276
|
published package:
|
|
236
277
|
|
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
|
@@ -1195,6 +1195,37 @@ export class AudientiClient {
|
|
|
1195
1195
|
});
|
|
1196
1196
|
}
|
|
1197
1197
|
|
|
1198
|
+
// Standalone account tool runs (#2554).
|
|
1199
|
+
createToolRun(accountId, body) {
|
|
1200
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs"]), { method: "POST", body });
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
toolRuns(accountId, query = {}) {
|
|
1204
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs"], query));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
toolRun(accountId, runId) {
|
|
1208
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs", runId]));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
toolRunResults(accountId, runId, query = {}) {
|
|
1212
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs", runId, "results"], query));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// CSV text by default; export_format=json returns an object.
|
|
1216
|
+
toolRunExport(accountId, runId, query = {}) {
|
|
1217
|
+
return this.requestJson(accountPath(accountId, ["tools", "runs", runId, "export"], query));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Saved network of one connected account (owner-only).
|
|
1221
|
+
socialCookieNetwork(accountId, socialCookieId, query = {}) {
|
|
1222
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "network"], query));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
socialCookieNetworkExport(accountId, socialCookieId, query = {}) {
|
|
1226
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "network", "export"], query));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1198
1229
|
humanizeText(accountId, body) {
|
|
1199
1230
|
return this.requestJson(accountPath(accountId, ["tools", "humanize"]), {
|
|
1200
1231
|
method: "POST",
|
|
@@ -1342,7 +1373,8 @@ export class AudientiClient {
|
|
|
1342
1373
|
|
|
1343
1374
|
headers(body) {
|
|
1344
1375
|
const headers = {
|
|
1345
|
-
Accept: "application/json"
|
|
1376
|
+
Accept: "application/json",
|
|
1377
|
+
"X-Audienti-Client": "cli"
|
|
1346
1378
|
};
|
|
1347
1379
|
|
|
1348
1380
|
if (this.token) {
|
|
@@ -1421,7 +1453,7 @@ function errorMessage(status, body) {
|
|
|
1421
1453
|
return details ? `Audienti rejected the request: ${details}` : "Audienti rejected the request.";
|
|
1422
1454
|
}
|
|
1423
1455
|
|
|
1424
|
-
if (status === 429 && body?.error) {
|
|
1456
|
+
if ((status === 429 || status === 503) && body?.error) {
|
|
1425
1457
|
return body.error;
|
|
1426
1458
|
}
|
|
1427
1459
|
|
package/src/cli.js
CHANGED
|
@@ -214,7 +214,14 @@ const ANALYTICS_METRICS_USAGE = "Usage: audienti analytics metrics [--cohort-sta
|
|
|
214
214
|
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
215
|
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
216
|
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>]";
|
|
217
|
+
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>]";
|
|
218
|
+
const TOOL_RUN_FLAGS_USAGE = "[--request-key <key>] [--wait [--timeout-seconds <n>] [--poll-interval-seconds <n>]] [--json] [--account <acct_id>]";
|
|
219
|
+
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}`;
|
|
220
|
+
const TOOLS_LINKEDIN_ENRICH_USAGE = `Usage: audienti tools linkedin-enrich (--url <linkedin_url> [--kind <person|company>] | --file <csv|jsonl|json>) ${TOOL_RUN_FLAGS_USAGE}`;
|
|
221
|
+
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}`;
|
|
222
|
+
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}`;
|
|
223
|
+
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>]";
|
|
224
|
+
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
225
|
const TOOLS_LINKEDIN_REVIEW_USAGE = "Usage: audienti tools linkedin-review --url <linkedin_url> [--icp <icp_id>] [--json] [--account <acct_id>]";
|
|
219
226
|
const TOOLS_LINKEDIN_REVIEW_REPORTS_USAGE = "Usage: audienti tools linkedin-review reports [--limit <n>] [--json] [--account <acct_id>]";
|
|
220
227
|
const TOOLS_LINKEDIN_REVIEW_SHOW_USAGE = "Usage: audienti tools linkedin-review show <rprt_id> [--json] [--account <acct_id>]";
|
|
@@ -480,6 +487,12 @@ async function dispatch(argv, context) {
|
|
|
480
487
|
if (normalizedResource === "tools" && action === "list") return toolsList(rest, context);
|
|
481
488
|
if (normalizedResource === "tools" && action === "get") return toolsGet(rest, context, { accountOverride });
|
|
482
489
|
if (normalizedResource === "tools" && action === "humanize") return toolsHumanize(rest, context, { accountOverride });
|
|
490
|
+
if (normalizedResource === "tools" && action === "email-find") return toolsEmailFind(rest, context, { accountOverride });
|
|
491
|
+
if (normalizedResource === "tools" && action === "linkedin-enrich") return toolsLinkedinEnrich(rest, context, { accountOverride });
|
|
492
|
+
if (normalizedResource === "tools" && action === "signals-find") return toolsSignalsFind(rest, context, { accountOverride });
|
|
493
|
+
if (normalizedResource === "tools" && action === "write") return toolsWrite(rest, context, { accountOverride });
|
|
494
|
+
if (normalizedResource === "tools" && action === "runs") return toolsRuns(rest, context, { accountOverride });
|
|
495
|
+
if (normalizedResource === "network") return networkCommand(action, rest, context, { accountOverride });
|
|
483
496
|
if (normalizedResource === "tools" && action === "linkedin-review") return toolsLinkedinReview(rest, context, { accountOverride });
|
|
484
497
|
if (normalizedResource === "tools" && action === "linkedin-strategy-review") return toolsLinkedinStrategyReview(rest, context, { accountOverride });
|
|
485
498
|
if (normalizedResource === "operator" && action === "failed-drafts") return operatorFailedDrafts(rest, context, { accountOverride });
|
|
@@ -4822,16 +4835,21 @@ async function toolsList(args, context) {
|
|
|
4822
4835
|
async function toolsHumanize(args, context, { accountOverride } = {}) {
|
|
4823
4836
|
const { values, positionals } = parseCommandArgs(args, {
|
|
4824
4837
|
...jsonOptions(),
|
|
4838
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
4825
4839
|
file: { type: "string" },
|
|
4826
4840
|
tone: { type: "string" },
|
|
4827
|
-
language: { type: "string" }
|
|
4841
|
+
language: { type: "string" },
|
|
4842
|
+
async: { type: "boolean" }
|
|
4828
4843
|
});
|
|
4829
4844
|
|
|
4830
|
-
if (positionals.length > 0 || !values.file) {
|
|
4845
|
+
if (positionals.length > 0 || !values.file || (values.wait && !values.async)) {
|
|
4831
4846
|
throw new CommandError(TOOLS_HUMANIZE_USAGE);
|
|
4832
4847
|
}
|
|
4833
4848
|
|
|
4834
4849
|
const text = await readHumanizerFile(values.file);
|
|
4850
|
+
if (values.async) {
|
|
4851
|
+
return submitToolRun("humanize", compactObject({ text, tone: values.tone, language: values.language }), values, context, { accountOverride });
|
|
4852
|
+
}
|
|
4835
4853
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
4836
4854
|
const payload = await client.humanizeText(accountId, {
|
|
4837
4855
|
text,
|
|
@@ -8562,6 +8580,41 @@ function availableTools() {
|
|
|
8562
8580
|
reports_command: null,
|
|
8563
8581
|
status_command: null
|
|
8564
8582
|
},
|
|
8583
|
+
{
|
|
8584
|
+
id: "email-find",
|
|
8585
|
+
command: "audienti tools email-find --first-name <name> --last-name <name> --company-domain <domain> | --linkedin-url <url> | --file <path>",
|
|
8586
|
+
description: "Find work emails without creating prospects. Returns a run id; add --wait for results.",
|
|
8587
|
+
reports_command: "audienti tools runs list --tool email-find",
|
|
8588
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8589
|
+
},
|
|
8590
|
+
{
|
|
8591
|
+
id: "linkedin-enrich",
|
|
8592
|
+
command: "audienti tools linkedin-enrich --url <linkedin_url> [--kind person|company] | --file <path>",
|
|
8593
|
+
description: "Enrich LinkedIn person or company URLs without creating prospects.",
|
|
8594
|
+
reports_command: "audienti tools runs list --tool linkedin-enrich",
|
|
8595
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8596
|
+
},
|
|
8597
|
+
{
|
|
8598
|
+
id: "signals-find",
|
|
8599
|
+
command: "audienti tools signals-find --icp <text> --question <text> [--count <n>]",
|
|
8600
|
+
description: "Find companies matching an ICP and a signal question, with source links.",
|
|
8601
|
+
reports_command: "audienti tools runs list --tool signals-find",
|
|
8602
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8603
|
+
},
|
|
8604
|
+
{
|
|
8605
|
+
id: "write",
|
|
8606
|
+
command: "audienti tools write --purpose <text> --audience <text> --fact <text> --channel <channel>",
|
|
8607
|
+
description: "Write one draft from a brief. Nothing is sent.",
|
|
8608
|
+
reports_command: "audienti tools runs list --tool write",
|
|
8609
|
+
status_command: "audienti tools runs show <trun_id>"
|
|
8610
|
+
},
|
|
8611
|
+
{
|
|
8612
|
+
id: "network",
|
|
8613
|
+
command: "audienti network list --cookie <scok_id> [--kind connection|follow]",
|
|
8614
|
+
description: "List or export your saved connections, followers and following (owner only).",
|
|
8615
|
+
reports_command: null,
|
|
8616
|
+
status_command: null
|
|
8617
|
+
},
|
|
8565
8618
|
{
|
|
8566
8619
|
id: "linkedin-review",
|
|
8567
8620
|
command: "audienti tools linkedin-review --url <linkedin_url> [--icp <icp_id>]",
|
|
@@ -13828,7 +13881,13 @@ const HELP_TOPICS = new Map([
|
|
|
13828
13881
|
"Usage:",
|
|
13829
13882
|
" audienti tools list [--json]",
|
|
13830
13883
|
" audienti tools get <email|phone> --url <linkedin_url> [--json]",
|
|
13831
|
-
" audienti tools humanize --file <path> [--tone <tone>] [--language <language>] [--json]",
|
|
13884
|
+
" audienti tools humanize --file <path> [--tone <tone>] [--language <language>] [--async [--wait]] [--json]",
|
|
13885
|
+
" audienti tools email-find --first-name <name> --last-name <name> --company-domain <domain> [--wait] [--json]",
|
|
13886
|
+
" audienti tools email-find --linkedin-url <url> | --file <csv|jsonl|json> [--wait] [--json]",
|
|
13887
|
+
" audienti tools linkedin-enrich --url <linkedin_url> [--kind <person|company>] | --file <path> [--wait] [--json]",
|
|
13888
|
+
" audienti tools signals-find --icp <text> --question <text> [--count <n>] [--date-window-days <n>] [--wait] [--json]",
|
|
13889
|
+
" audienti tools write --purpose <text> --audience <text> --fact <text> --channel <channel> [--wait] [--json]",
|
|
13890
|
+
" audienti tools runs list|show <trun_id>|results <trun_id>|export <trun_id> [--json]",
|
|
13832
13891
|
" audienti tools linkedin-review --url <linkedin_url> [--icp <icp_id>] [--json]",
|
|
13833
13892
|
" audienti tools linkedin-review reports [--limit <n>] [--json]",
|
|
13834
13893
|
" audienti tools linkedin-review show <rprt_id> [--json]",
|
|
@@ -13844,6 +13903,11 @@ const HELP_TOPICS = new Map([
|
|
|
13844
13903
|
" audienti tools list Show available CLI tools and the report commands they support.",
|
|
13845
13904
|
" audienti tools get Run a LinkedIn URL through the existing import and contact-enrichment pipeline, then return the first selected email or phone.",
|
|
13846
13905
|
" audienti tools humanize Humanize arbitrary UTF-8 text and print the result.",
|
|
13906
|
+
" audienti tools email-find Find work emails without creating prospects (returns a run id).",
|
|
13907
|
+
" audienti tools linkedin-enrich Enrich LinkedIn person or company URLs without creating prospects.",
|
|
13908
|
+
" audienti tools signals-find Find companies matching an ICP and a signal question, with sources.",
|
|
13909
|
+
" audienti tools write Write one draft from a brief. Nothing is sent.",
|
|
13910
|
+
" audienti tools runs List tool runs and read their status, results and exports.",
|
|
13847
13911
|
" audienti tools linkedin-review Queue a LinkedIn personal profile authority review and ICP-fit positioning blueprint.",
|
|
13848
13912
|
" audienti tools linkedin-review reports List recent LinkedIn Review reports for the active account.",
|
|
13849
13913
|
" audienti tools linkedin-review show View the completed report content in the terminal.",
|
|
@@ -13908,12 +13972,15 @@ const HELP_TOPICS = new Map([
|
|
|
13908
13972
|
" --tone <professional|academic|blog|casual|creative|scientific|technical>",
|
|
13909
13973
|
" --language <name> Optional language hint, for example English",
|
|
13910
13974
|
"",
|
|
13975
|
+
" --async Submit as a tool run and print the run id instead (add --wait to poll for the result)",
|
|
13976
|
+
"",
|
|
13911
13977
|
"Output:",
|
|
13912
13978
|
" Plain text: only the humanized text, suitable for redirecting to a file",
|
|
13913
|
-
" JSON: { humanized_text, id, input_words }",
|
|
13979
|
+
" JSON: { humanized_text, id, input_words, run_id }",
|
|
13914
13980
|
"",
|
|
13915
13981
|
"API:",
|
|
13916
|
-
" POST /api/v1/accounts/:account_id/tools/humanize.json"
|
|
13982
|
+
" POST /api/v1/accounts/:account_id/tools/humanize.json",
|
|
13983
|
+
" POST /api/v1/accounts/:account_id/tools/runs.json (--async)"
|
|
13917
13984
|
].join("\n")],
|
|
13918
13985
|
|
|
13919
13986
|
["tools linkedin-review", [
|
|
@@ -15507,3 +15574,476 @@ async function runParityActionCommand(topic, args, context, { accountOverride }
|
|
|
15507
15574
|
|
|
15508
15575
|
writeLine(context.stdout, spec.done(positionals, payload));
|
|
15509
15576
|
}
|
|
15577
|
+
|
|
15578
|
+
// ---------------------------------------------------------------------------
|
|
15579
|
+
// Standalone account tools and saved network (#2554). Thin client of the
|
|
15580
|
+
// tools/runs and social_cookies/:id/network APIs: submit returns a run id
|
|
15581
|
+
// immediately; --wait polls the run, then prints its results.
|
|
15582
|
+
// ---------------------------------------------------------------------------
|
|
15583
|
+
|
|
15584
|
+
const TOOL_RUN_WAIT_OPTIONS = {
|
|
15585
|
+
"request-key": { type: "string" },
|
|
15586
|
+
wait: { type: "boolean" },
|
|
15587
|
+
"timeout-seconds": { type: "string" },
|
|
15588
|
+
"poll-interval-seconds": { type: "string" }
|
|
15589
|
+
};
|
|
15590
|
+
const DEFAULT_TOOL_RUN_TIMEOUT_SECONDS = 300;
|
|
15591
|
+
const DEFAULT_TOOL_RUN_POLL_INTERVAL_SECONDS = 2;
|
|
15592
|
+
const TOOL_RUN_FINISHED_STATUSES = ["completed", "failed"];
|
|
15593
|
+
|
|
15594
|
+
async function toolsEmailFind(args, context, { accountOverride } = {}) {
|
|
15595
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15596
|
+
...jsonOptions(),
|
|
15597
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15598
|
+
"first-name": { type: "string" },
|
|
15599
|
+
"last-name": { type: "string" },
|
|
15600
|
+
"company-domain": { type: "string" },
|
|
15601
|
+
"linkedin-url": { type: "string" },
|
|
15602
|
+
file: { type: "string" }
|
|
15603
|
+
});
|
|
15604
|
+
if (positionals.length > 0) throw new CommandError(TOOLS_EMAIL_FIND_USAGE);
|
|
15605
|
+
|
|
15606
|
+
const single = compactObject({
|
|
15607
|
+
first_name: values["first-name"],
|
|
15608
|
+
last_name: values["last-name"],
|
|
15609
|
+
company_domain: values["company-domain"],
|
|
15610
|
+
linkedin_url: values["linkedin-url"]
|
|
15611
|
+
});
|
|
15612
|
+
const items = values.file
|
|
15613
|
+
? await readToolItemsFile(values.file, ["client_item_id", "first_name", "last_name", "company_domain", "linkedin_url"])
|
|
15614
|
+
: [single];
|
|
15615
|
+
if (values.file && Object.keys(single).length > 0) throw new CommandError("Use either --file or the single-lookup flags, not both.");
|
|
15616
|
+
if (!values.file && !single.linkedin_url && !(single.first_name && single.last_name && single.company_domain)) {
|
|
15617
|
+
throw new CommandError(TOOLS_EMAIL_FIND_USAGE);
|
|
15618
|
+
}
|
|
15619
|
+
|
|
15620
|
+
return submitToolRun("email_find", { items }, values, context, { accountOverride });
|
|
15621
|
+
}
|
|
15622
|
+
|
|
15623
|
+
async function toolsLinkedinEnrich(args, context, { accountOverride } = {}) {
|
|
15624
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15625
|
+
...jsonOptions(),
|
|
15626
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15627
|
+
url: { type: "string" },
|
|
15628
|
+
kind: { type: "string" },
|
|
15629
|
+
file: { type: "string" }
|
|
15630
|
+
});
|
|
15631
|
+
if (positionals.length > 0 || (!values.url && !values.file) || (values.url && values.file)) {
|
|
15632
|
+
throw new CommandError(TOOLS_LINKEDIN_ENRICH_USAGE);
|
|
15633
|
+
}
|
|
15634
|
+
if (values.kind && !["person", "company"].includes(values.kind)) throw new CommandError("--kind must be person or company.");
|
|
15635
|
+
|
|
15636
|
+
const items = values.file
|
|
15637
|
+
? await readToolItemsFile(values.file, ["client_item_id", "linkedin_url", "kind"], { aliases: { url: "linkedin_url" } })
|
|
15638
|
+
: [compactObject({ linkedin_url: values.url, kind: values.kind })];
|
|
15639
|
+
return submitToolRun("linkedin_enrich", { items }, values, context, { accountOverride });
|
|
15640
|
+
}
|
|
15641
|
+
|
|
15642
|
+
async function toolsSignalsFind(args, context, { accountOverride } = {}) {
|
|
15643
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15644
|
+
...jsonOptions(),
|
|
15645
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15646
|
+
icp: { type: "string" },
|
|
15647
|
+
question: { type: "string" },
|
|
15648
|
+
count: { type: "string" },
|
|
15649
|
+
"date-window-days": { type: "string" }
|
|
15650
|
+
});
|
|
15651
|
+
if (positionals.length > 0 || !values.icp || !values.question) throw new CommandError(TOOLS_SIGNALS_FIND_USAGE);
|
|
15652
|
+
|
|
15653
|
+
const input = compactObject({
|
|
15654
|
+
icp_description: values.icp,
|
|
15655
|
+
question: values.question,
|
|
15656
|
+
count: normalizeOptionalPositiveInteger(values.count, "--count"),
|
|
15657
|
+
date_window_days: normalizeOptionalPositiveInteger(values["date-window-days"], "--date-window-days")
|
|
15658
|
+
});
|
|
15659
|
+
return submitToolRun("signals_find", input, values, context, { accountOverride });
|
|
15660
|
+
}
|
|
15661
|
+
|
|
15662
|
+
async function toolsWrite(args, context, { accountOverride } = {}) {
|
|
15663
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15664
|
+
...jsonOptions(),
|
|
15665
|
+
...TOOL_RUN_WAIT_OPTIONS,
|
|
15666
|
+
purpose: { type: "string" },
|
|
15667
|
+
audience: { type: "string" },
|
|
15668
|
+
fact: { type: "string", multiple: true },
|
|
15669
|
+
channel: { type: "string" },
|
|
15670
|
+
tone: { type: "string" },
|
|
15671
|
+
subject: { type: "boolean" },
|
|
15672
|
+
"no-subject": { type: "boolean" },
|
|
15673
|
+
prospect: { type: "string" }
|
|
15674
|
+
});
|
|
15675
|
+
const facts = (values.fact || []).map((fact) => fact.trim()).filter(Boolean);
|
|
15676
|
+
if (positionals.length > 0 || !values.purpose || !values.audience || !values.channel || facts.length === 0) {
|
|
15677
|
+
throw new CommandError(TOOLS_WRITE_USAGE);
|
|
15678
|
+
}
|
|
15679
|
+
if (values.subject && values["no-subject"]) throw new CommandError("Choose either --subject or --no-subject.");
|
|
15680
|
+
|
|
15681
|
+
const input = compactObject({
|
|
15682
|
+
purpose: values.purpose,
|
|
15683
|
+
audience: values.audience,
|
|
15684
|
+
facts,
|
|
15685
|
+
channel: values.channel,
|
|
15686
|
+
tone: values.tone,
|
|
15687
|
+
prospect_id: values.prospect,
|
|
15688
|
+
subject: values.subject ? true : (values["no-subject"] ? false : undefined)
|
|
15689
|
+
});
|
|
15690
|
+
return submitToolRun("write", input, values, context, { accountOverride });
|
|
15691
|
+
}
|
|
15692
|
+
|
|
15693
|
+
async function submitToolRun(tool, input, values, context, { accountOverride } = {}) {
|
|
15694
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15695
|
+
const run = await client.createToolRun(accountId, compactObject({ tool, input, request_key: values["request-key"] }));
|
|
15696
|
+
if (!values.wait) {
|
|
15697
|
+
if (values.json) return writeJson(context.stdout, run);
|
|
15698
|
+
|
|
15699
|
+
writeLine(context.stdout, `${run?.replayed ? "Existing" : "Queued"} run ${display(run?.id)} (${tool}, ${display(run?.item_count, 0)} item(s)).`);
|
|
15700
|
+
writeLine(context.stdout, `Check it with: audienti tools runs show ${display(run?.id)}`);
|
|
15701
|
+
return 0;
|
|
15702
|
+
}
|
|
15703
|
+
|
|
15704
|
+
return finishWaitedRun(client, accountId, run, values, context);
|
|
15705
|
+
}
|
|
15706
|
+
|
|
15707
|
+
async function finishWaitedRun(client, accountId, run, values, context) {
|
|
15708
|
+
const finished = await waitForToolRun(client, accountId, run, {
|
|
15709
|
+
timeoutSeconds: normalizeOptionalPositiveInteger(values["timeout-seconds"], "--timeout-seconds") || DEFAULT_TOOL_RUN_TIMEOUT_SECONDS,
|
|
15710
|
+
pollIntervalSeconds: normalizeOptionalPositiveInteger(values["poll-interval-seconds"], "--poll-interval-seconds") || DEFAULT_TOOL_RUN_POLL_INTERVAL_SECONDS,
|
|
15711
|
+
sleepImpl: context.sleep
|
|
15712
|
+
});
|
|
15713
|
+
const items = await fetchAllToolRunItems(client, accountId, finished.id);
|
|
15714
|
+
if (values.json) {
|
|
15715
|
+
writeJson(context.stdout, { run: finished, items });
|
|
15716
|
+
} else {
|
|
15717
|
+
renderToolRun(finished, context);
|
|
15718
|
+
renderToolRunItems(finished.tool, items, context);
|
|
15719
|
+
}
|
|
15720
|
+
return finished.status === "failed" ? 1 : 0;
|
|
15721
|
+
}
|
|
15722
|
+
|
|
15723
|
+
async function waitForToolRun(client, accountId, run, { timeoutSeconds, pollIntervalSeconds, sleepImpl = sleep }) {
|
|
15724
|
+
if (!run?.id) throw new CommandError("The tool run response did not include a run id.");
|
|
15725
|
+
if (TOOL_RUN_FINISHED_STATUSES.includes(run.status)) return run;
|
|
15726
|
+
|
|
15727
|
+
const timeoutAt = Date.now() + (timeoutSeconds * 1000);
|
|
15728
|
+
let latest = run;
|
|
15729
|
+
while (Date.now() < timeoutAt) {
|
|
15730
|
+
await sleepImpl(pollIntervalSeconds * 1000);
|
|
15731
|
+
latest = await client.toolRun(accountId, run.id);
|
|
15732
|
+
if (TOOL_RUN_FINISHED_STATUSES.includes(latest?.status)) return latest;
|
|
15733
|
+
}
|
|
15734
|
+
|
|
15735
|
+
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}`);
|
|
15736
|
+
}
|
|
15737
|
+
|
|
15738
|
+
async function fetchAllToolRunItems(client, accountId, runId) {
|
|
15739
|
+
const items = [];
|
|
15740
|
+
let cursor;
|
|
15741
|
+
for (let page = 0; page < 100; page += 1) {
|
|
15742
|
+
const payload = await client.toolRunResults(accountId, runId, compactObject({ limit: 200, cursor }));
|
|
15743
|
+
items.push(...(Array.isArray(payload?.items) ? payload.items : []));
|
|
15744
|
+
cursor = payload?.next_cursor;
|
|
15745
|
+
if (!cursor) break;
|
|
15746
|
+
}
|
|
15747
|
+
return items;
|
|
15748
|
+
}
|
|
15749
|
+
|
|
15750
|
+
async function toolsRuns(args, context, { accountOverride } = {}) {
|
|
15751
|
+
const [subcommand, ...rest] = args;
|
|
15752
|
+
if (subcommand === "list") return toolsRunsList(rest, context, { accountOverride });
|
|
15753
|
+
if (subcommand === "show") return toolsRunsShow(rest, context, { accountOverride });
|
|
15754
|
+
if (subcommand === "results") return toolsRunsResults(rest, context, { accountOverride });
|
|
15755
|
+
if (subcommand === "export") return toolsRunsExport(rest, context, { accountOverride });
|
|
15756
|
+
|
|
15757
|
+
throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15758
|
+
}
|
|
15759
|
+
|
|
15760
|
+
async function toolsRunsList(args, context, { accountOverride } = {}) {
|
|
15761
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15762
|
+
...jsonOptions(),
|
|
15763
|
+
tool: { type: "string" },
|
|
15764
|
+
status: { type: "string" },
|
|
15765
|
+
limit: { type: "string" },
|
|
15766
|
+
cursor: { type: "string" }
|
|
15767
|
+
});
|
|
15768
|
+
if (positionals.length > 0) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15769
|
+
|
|
15770
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15771
|
+
const payload = await client.toolRuns(accountId, compactObject({
|
|
15772
|
+
tool: values.tool ? values.tool.replaceAll("-", "_") : undefined,
|
|
15773
|
+
status: values.status,
|
|
15774
|
+
limit: normalizeOptionalPositiveInteger(values.limit, "--limit"),
|
|
15775
|
+
cursor: values.cursor
|
|
15776
|
+
}));
|
|
15777
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
15778
|
+
|
|
15779
|
+
const runs = Array.isArray(payload?.runs) ? payload.runs : [];
|
|
15780
|
+
if (runs.length === 0) return writeLine(context.stdout, "No tool runs.");
|
|
15781
|
+
writeAlignedTable(context, ["ID", "TOOL", "STATUS", "ITEMS", "CREATED"],
|
|
15782
|
+
runs.map((run) => [run.id, run.tool, run.details_expired ? `${run.status} (details expired)` : run.status, run.item_count, run.created_at]));
|
|
15783
|
+
if (payload?.next_cursor) writeLine(context.stdout, `More runs: audienti tools runs list --cursor ${payload.next_cursor}`);
|
|
15784
|
+
}
|
|
15785
|
+
|
|
15786
|
+
async function toolsRunsShow(args, context, { accountOverride } = {}) {
|
|
15787
|
+
const { values, positionals } = parseCommandArgs(args, jsonOptions());
|
|
15788
|
+
if (positionals.length !== 1) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15789
|
+
|
|
15790
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15791
|
+
const run = await client.toolRun(accountId, positionals[0]);
|
|
15792
|
+
if (values.json) return writeJson(context.stdout, run);
|
|
15793
|
+
|
|
15794
|
+
renderToolRun(run, context);
|
|
15795
|
+
return run?.status === "failed" ? 1 : 0;
|
|
15796
|
+
}
|
|
15797
|
+
|
|
15798
|
+
async function toolsRunsResults(args, context, { accountOverride } = {}) {
|
|
15799
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15800
|
+
...jsonOptions(),
|
|
15801
|
+
limit: { type: "string" },
|
|
15802
|
+
cursor: { type: "string" }
|
|
15803
|
+
});
|
|
15804
|
+
if (positionals.length !== 1) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15805
|
+
|
|
15806
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15807
|
+
const payload = await client.toolRunResults(accountId, positionals[0], compactObject({
|
|
15808
|
+
limit: normalizeOptionalPositiveInteger(values.limit, "--limit"),
|
|
15809
|
+
cursor: values.cursor
|
|
15810
|
+
}));
|
|
15811
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
15812
|
+
|
|
15813
|
+
if (payload?.details_expired) return writeLine(context.stdout, "This run's details expired after 90 days; only its summary remains.");
|
|
15814
|
+
renderToolRunItems(payload?.run?.tool, Array.isArray(payload?.items) ? payload.items : [], context);
|
|
15815
|
+
if (payload?.next_cursor) writeLine(context.stdout, `More results: audienti tools runs results ${positionals[0]} --cursor ${payload.next_cursor}`);
|
|
15816
|
+
}
|
|
15817
|
+
|
|
15818
|
+
async function toolsRunsExport(args, context, { accountOverride } = {}) {
|
|
15819
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15820
|
+
format: { type: "string" },
|
|
15821
|
+
output: { type: "string" }
|
|
15822
|
+
});
|
|
15823
|
+
if (positionals.length !== 1) throw new CommandError(TOOLS_RUNS_USAGE);
|
|
15824
|
+
const format = exportFormat(values.format);
|
|
15825
|
+
|
|
15826
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15827
|
+
const payload = await client.toolRunExport(accountId, positionals[0], format === "json" ? { export_format: "json" } : {});
|
|
15828
|
+
return writeExportPayload(payload, format, values.output, context);
|
|
15829
|
+
}
|
|
15830
|
+
|
|
15831
|
+
async function networkCommand(action, args, context, { accountOverride } = {}) {
|
|
15832
|
+
if (!["list", "export"].includes(action)) throw new CommandError(NETWORK_USAGE);
|
|
15833
|
+
|
|
15834
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
15835
|
+
...(action === "list" ? jsonOptions() : {}),
|
|
15836
|
+
cookie: { type: "string" },
|
|
15837
|
+
platform: { type: "string" },
|
|
15838
|
+
kind: { type: "string" },
|
|
15839
|
+
direction: { type: "string" },
|
|
15840
|
+
query: { type: "string" },
|
|
15841
|
+
...(action === "list" ? { limit: { type: "string" }, cursor: { type: "string" } } : { format: { type: "string" }, output: { type: "string" } })
|
|
15842
|
+
});
|
|
15843
|
+
if (positionals.length > 0 || !values.cookie) throw new CommandError(NETWORK_USAGE);
|
|
15844
|
+
|
|
15845
|
+
const query = compactObject({
|
|
15846
|
+
platform: values.platform || "linkedin",
|
|
15847
|
+
kind: values.kind || "connection",
|
|
15848
|
+
direction: values.direction,
|
|
15849
|
+
network_query: values.query
|
|
15850
|
+
});
|
|
15851
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
15852
|
+
|
|
15853
|
+
if (action === "export") {
|
|
15854
|
+
const format = exportFormat(values.format);
|
|
15855
|
+
const payload = await client.socialCookieNetworkExport(accountId, values.cookie, format === "json" ? { ...query, export_format: "json" } : query);
|
|
15856
|
+
return writeExportPayload(payload, format, values.output, context);
|
|
15857
|
+
}
|
|
15858
|
+
|
|
15859
|
+
const payload = await client.socialCookieNetwork(accountId, values.cookie, compactObject({
|
|
15860
|
+
...query,
|
|
15861
|
+
limit: normalizeOptionalPositiveInteger(values.limit, "--limit"),
|
|
15862
|
+
cursor: values.cursor
|
|
15863
|
+
}));
|
|
15864
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
15865
|
+
|
|
15866
|
+
renderNetworkCoverage(payload?.coverage, context);
|
|
15867
|
+
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
|
|
15868
|
+
if (rows.length > 0) {
|
|
15869
|
+
writeAlignedTable(context, ["NAME", "USERNAME", "ID", "LAST SEEN"],
|
|
15870
|
+
rows.map((row) => [row.display_name, row.alias_key, row.stable_id, row.last_observed_at]));
|
|
15871
|
+
}
|
|
15872
|
+
if (payload?.next_cursor) writeLine(context.stdout, `More rows: audienti network list --cookie ${values.cookie} --cursor ${payload.next_cursor}`);
|
|
15873
|
+
}
|
|
15874
|
+
|
|
15875
|
+
function renderNetworkCoverage(coverage, context) {
|
|
15876
|
+
if (!coverage) return;
|
|
15877
|
+
|
|
15878
|
+
const state = {
|
|
15879
|
+
unsupported_capture: "capture not supported yet (not zero)",
|
|
15880
|
+
not_yet_observed: "not yet observed",
|
|
15881
|
+
partial: "partial (only what normal work has seen)",
|
|
15882
|
+
complete: "complete"
|
|
15883
|
+
}[coverage.state] || display(coverage.state);
|
|
15884
|
+
writeLine(context.stdout, `${display(coverage.platform)} ${display(coverage.kind)} ${display(coverage.direction)}: ${state}` +
|
|
15885
|
+
(coverage.last_observed_at ? `, last seen ${coverage.last_observed_at}` : ""));
|
|
15886
|
+
}
|
|
15887
|
+
|
|
15888
|
+
function exportFormat(value) {
|
|
15889
|
+
const format = String(value || "csv").toLowerCase();
|
|
15890
|
+
if (!["csv", "json"].includes(format)) throw new CommandError("--format must be csv or json.");
|
|
15891
|
+
return format;
|
|
15892
|
+
}
|
|
15893
|
+
|
|
15894
|
+
async function writeExportPayload(payload, format, outputPath, context) {
|
|
15895
|
+
const text = format === "json" ? `${JSON.stringify(payload, null, 2)}\n` : String(payload ?? "");
|
|
15896
|
+
if (outputPath) {
|
|
15897
|
+
await writeFile(outputPath, text);
|
|
15898
|
+
return writeLine(context.stderr, `Wrote ${Buffer.byteLength(text)} bytes to ${outputPath}`);
|
|
15899
|
+
}
|
|
15900
|
+
context.stdout.write(text);
|
|
15901
|
+
}
|
|
15902
|
+
|
|
15903
|
+
function renderToolRun(run, context) {
|
|
15904
|
+
writeLine(context.stdout, `Run ${display(run?.id)} (${display(run?.tool)}): ${display(run?.status)}`);
|
|
15905
|
+
writeLine(context.stdout, `Items: ${display(run?.item_count, 0)} total, ${display(run?.completed_item_count, 0)} completed, ${display(run?.failed_item_count, 0)} failed`);
|
|
15906
|
+
const outcomes = run?.summary?.outcomes || {};
|
|
15907
|
+
if (Object.keys(outcomes).length > 0) {
|
|
15908
|
+
writeLine(context.stdout, `Outcomes: ${Object.entries(outcomes).map(([outcome, count]) => `${outcome} ${count}`).join(", ")}`);
|
|
15909
|
+
}
|
|
15910
|
+
if (run?.error_code) writeLine(context.stdout, `Error: ${run.error_code}`);
|
|
15911
|
+
if (run?.details_expired) writeLine(context.stdout, "Details expired after 90 days; only this summary remains.");
|
|
15912
|
+
}
|
|
15913
|
+
|
|
15914
|
+
function renderToolRunItems(tool, items, context) {
|
|
15915
|
+
if (items.length === 0) return writeLine(context.stdout, "No results yet.");
|
|
15916
|
+
|
|
15917
|
+
writeAlignedTable(context, ["#", "ITEM", "OUTCOME", "RESULT"],
|
|
15918
|
+
items.map((item) => [item.position, item.client_item_id, item.outcome || item.status, toolItemSummary(tool, item)]));
|
|
15919
|
+
}
|
|
15920
|
+
|
|
15921
|
+
function toolItemSummary(tool, item) {
|
|
15922
|
+
const result = item?.result || {};
|
|
15923
|
+
if (item?.error_message && item.outcome !== "success") return item.error_message;
|
|
15924
|
+
if (tool === "email_find") return result.email || "";
|
|
15925
|
+
if (tool === "linkedin_enrich") return [result.display_name || result.name, result.headline || result.domain].filter(Boolean).join(" - ");
|
|
15926
|
+
if (tool === "signals_find") return (result.companies || []).map((company) => company.name).join(", ");
|
|
15927
|
+
if (tool === "write") return singleLine([result.subject, result.text].filter(Boolean).join(" | "));
|
|
15928
|
+
if (tool === "humanize") return singleLine(result.humanized_text);
|
|
15929
|
+
if (tool === "network_export") return `${display(result.exported_row_count, 0)} row(s)`;
|
|
15930
|
+
return Object.keys(result).length > 0 ? JSON.stringify(result) : "";
|
|
15931
|
+
}
|
|
15932
|
+
|
|
15933
|
+
// Reads tool items from CSV (header row), JSONL, a JSON array or {items: [...]}.
|
|
15934
|
+
async function readToolItemsFile(filePath, fields, { aliases = {} } = {}) {
|
|
15935
|
+
let contents;
|
|
15936
|
+
try {
|
|
15937
|
+
contents = await readFile(filePath, "utf8");
|
|
15938
|
+
} catch (error) {
|
|
15939
|
+
throw new CommandError(`Could not read items file ${filePath}: ${error.message}`);
|
|
15940
|
+
}
|
|
15941
|
+
const trimmed = String(contents || "").trim();
|
|
15942
|
+
if (!trimmed) throw new CommandError(`Items file ${filePath} is empty.`);
|
|
15943
|
+
|
|
15944
|
+
let rows;
|
|
15945
|
+
let whole;
|
|
15946
|
+
try {
|
|
15947
|
+
whole = JSON.parse(trimmed);
|
|
15948
|
+
} catch {
|
|
15949
|
+
whole = undefined;
|
|
15950
|
+
}
|
|
15951
|
+
if (whole !== undefined) {
|
|
15952
|
+
rows = Array.isArray(whole) ? whole : (Array.isArray(whole?.items) ? whole.items : [whole]);
|
|
15953
|
+
} else {
|
|
15954
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
15955
|
+
if (lines[0].startsWith("{") || lines[0].startsWith("[")) {
|
|
15956
|
+
rows = lines.map((line, index) => {
|
|
15957
|
+
try {
|
|
15958
|
+
return JSON.parse(line);
|
|
15959
|
+
} catch (error) {
|
|
15960
|
+
throw new CommandError(`Invalid JSON or JSONL row ${index + 1} in ${filePath}: ${error.message}`);
|
|
15961
|
+
}
|
|
15962
|
+
});
|
|
15963
|
+
} else {
|
|
15964
|
+
// CSV: one row per line; quoted commas allowed, no line breaks inside fields.
|
|
15965
|
+
const headers = parseCsvLine(lines[0]).map((header) => header.trim());
|
|
15966
|
+
rows = lines.slice(1).map((line) => {
|
|
15967
|
+
const values = parseCsvLine(line);
|
|
15968
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
|
|
15969
|
+
});
|
|
15970
|
+
}
|
|
15971
|
+
}
|
|
15972
|
+
|
|
15973
|
+
return rows.map((row) => {
|
|
15974
|
+
const normalized = {};
|
|
15975
|
+
for (const [rawKey, value] of Object.entries(row || {})) {
|
|
15976
|
+
const key = rawKey.trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
|
|
15977
|
+
const field = aliases[key] || key;
|
|
15978
|
+
if (fields.includes(field) && value !== undefined && value !== null) normalized[field] = String(value);
|
|
15979
|
+
}
|
|
15980
|
+
return compactObject(normalized);
|
|
15981
|
+
});
|
|
15982
|
+
}
|
|
15983
|
+
|
|
15984
|
+
const TOOL_RUN_HELP_FOOTER = [
|
|
15985
|
+
"",
|
|
15986
|
+
"Behavior:",
|
|
15987
|
+
" Returns a run id immediately; the work runs on the server. Nothing is sent and no prospects are created.",
|
|
15988
|
+
" --wait polls `audienti tools runs show` until the run finishes, then prints its results.",
|
|
15989
|
+
" If the CLI exits early, read the run later with `audienti tools runs show|results <trun_id>`.",
|
|
15990
|
+
" --request-key makes a retry safe: the same key and input return the same run.",
|
|
15991
|
+
"",
|
|
15992
|
+
"Files (--file):",
|
|
15993
|
+
" JSON (an array or {\"items\": [...]}, any formatting), JSONL (one object per line) or CSV with a header row.",
|
|
15994
|
+
" CSV is one row per line; quoted commas are allowed, line breaks inside fields are not.",
|
|
15995
|
+
"",
|
|
15996
|
+
"Exit codes:",
|
|
15997
|
+
" 1 on an API error, a --wait timeout or an aborted (failed) run; otherwise 0.",
|
|
15998
|
+
" Per-item outcomes such as no_match or invalid_input do not change the exit code; read them in the results.",
|
|
15999
|
+
"",
|
|
16000
|
+
"API:",
|
|
16001
|
+
" POST /api/v1/accounts/:account_id/tools/runs.json",
|
|
16002
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id.json",
|
|
16003
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id/results.json"
|
|
16004
|
+
];
|
|
16005
|
+
|
|
16006
|
+
for (const [topic, usage, purpose] of [
|
|
16007
|
+
["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."],
|
|
16008
|
+
["tools linkedin-enrich", TOOLS_LINKEDIN_ENRICH_USAGE, "Reads public LinkedIn person or company details through API providers. Never uses your connected LinkedIn account."],
|
|
16009
|
+
["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."],
|
|
16010
|
+
["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."]
|
|
16011
|
+
]) {
|
|
16012
|
+
HELP_TOPICS.set(topic, ["Usage:", ` ${usage.slice("Usage: ".length)}`, "", "Status: implemented", "", "Purpose:", ` ${purpose}`, ...TOOL_RUN_HELP_FOOTER].join("\n"));
|
|
16013
|
+
}
|
|
16014
|
+
|
|
16015
|
+
HELP_TOPICS.set("tools runs", [
|
|
16016
|
+
"Usage:",
|
|
16017
|
+
` ${TOOLS_RUNS_USAGE.slice("Usage: ".length)}`,
|
|
16018
|
+
"",
|
|
16019
|
+
"Status: implemented",
|
|
16020
|
+
"",
|
|
16021
|
+
"Purpose:",
|
|
16022
|
+
" Lists tool runs and reads one run's status, per-item results and export. Details expire after 90 days; the summary remains.",
|
|
16023
|
+
" Saved-network runs show their results only to the connected account's owner.",
|
|
16024
|
+
"",
|
|
16025
|
+
"API:",
|
|
16026
|
+
" GET /api/v1/accounts/:account_id/tools/runs.json",
|
|
16027
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id.json",
|
|
16028
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id/results.json",
|
|
16029
|
+
" GET /api/v1/accounts/:account_id/tools/runs/:id/export.json"
|
|
16030
|
+
].join("\n"));
|
|
16031
|
+
|
|
16032
|
+
HELP_TOPICS.set("network", [
|
|
16033
|
+
"Usage:",
|
|
16034
|
+
` ${NETWORK_USAGE.slice("Usage: ".length)}`,
|
|
16035
|
+
"",
|
|
16036
|
+
"Status: implemented",
|
|
16037
|
+
"",
|
|
16038
|
+
"Purpose:",
|
|
16039
|
+
" Lists, searches or exports the saved connections, followers and following of one connected account.",
|
|
16040
|
+
" Only that account's owner can read it. It shows what normal work has already seen; it never visits LinkedIn or X.",
|
|
16041
|
+
" Coverage says partial, not yet observed, or capture not supported yet; none of these mean zero.",
|
|
16042
|
+
"",
|
|
16043
|
+
"Defaults:",
|
|
16044
|
+
" --platform linkedin --kind connection (follows need --direction incoming|outgoing)",
|
|
16045
|
+
"",
|
|
16046
|
+
"API:",
|
|
16047
|
+
" GET /api/v1/accounts/:account_id/social_cookies/:social_cookie_id/network.json",
|
|
16048
|
+
" GET /api/v1/accounts/:account_id/social_cookies/:social_cookie_id/network/export.json"
|
|
16049
|
+
].join("\n"));
|