@audienti/cli 0.1.80 → 0.1.82
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 +13 -0
- package/package.json +1 -1
- package/src/api-client.js +63 -0
- package/src/cli.js +427 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to the Audienti CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.82] - 2026-09-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Run LinkedIn strategy reviews from the CLI: `tools linkedin-strategy-review list [--limit]`, `create --url <linkedin_profile_url>`, and `show <rprt_id>` (`--json` includes the report content), next to the existing `delete`.
|
|
12
|
+
|
|
13
|
+
## [0.1.81] - 2026-09-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Manage social accounts from the CLI: `social-cookies show`, `pause`, `resume`, `resume-autopilot`, `recheck-account-type`, `reconnect`, `delete`, `submit-otp`, `create`, `update`, `rights` (`--workspace --grant|--revoke` or `--workspaces`), and `settings --scope`.
|
|
18
|
+
- Secrets never go on the command line. Passwords, one-time codes, cookies, TOTP secrets, and messaging PINs come from stdin (`--password-stdin`, `--secrets-stdin`, `--otp-stdin`), an environment variable (`--password-env` reads `AUDIENTI_SOCIAL_PASSWORD`, `--otp-env` reads `AUDIENTI_OTP_CODE`), or a hidden terminal prompt. Flags such as `--password` or `--otp` are refused before any request is sent, and the CLI never prints a secret.
|
|
19
|
+
|
|
7
20
|
## [0.1.80] - 2026-09-28
|
|
8
21
|
|
|
9
22
|
### Added
|
package/package.json
CHANGED
package/src/api-client.js
CHANGED
|
@@ -74,6 +74,54 @@ export class AudientiClient {
|
|
|
74
74
|
return this.requestJson(accountPath(accountId, ["social_cookies"], query));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
socialCookie(accountId, socialCookieId) {
|
|
78
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createSocialCookie(accountId, body) {
|
|
82
|
+
return this.requestJson(accountPath(accountId, ["social_cookies"]), { method: "POST", body });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
updateSocialCookie(accountId, socialCookieId, body) {
|
|
86
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId]), { method: "PATCH", body });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
deleteSocialCookie(accountId, socialCookieId) {
|
|
90
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId]), { method: "DELETE" });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pauseSocialCookie(accountId, socialCookieId) {
|
|
94
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "pause"]), { method: "POST" });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resumeSocialCookie(accountId, socialCookieId) {
|
|
98
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "resume"]), { method: "POST" });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
resumeSocialCookieAutopilot(accountId, socialCookieId) {
|
|
102
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "resume_autopilot"]), { method: "POST" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
recheckSocialCookieAccountType(accountId, socialCookieId) {
|
|
106
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "recheck_account_type"]), { method: "POST" });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
reconnectSocialCookie(accountId, socialCookieId) {
|
|
110
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "reconnect"]), { method: "POST" });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
submitSocialCookieOtp(accountId, socialCookieId, body) {
|
|
114
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "submit_otp"]), { method: "POST", body });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updateSocialCookieRights(accountId, socialCookieId, body) {
|
|
118
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "rights"]), { method: "PATCH", body });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
updateSocialCookieSettings(accountId, socialCookieId, body) {
|
|
122
|
+
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "settings"]), { method: "PATCH", body });
|
|
123
|
+
}
|
|
124
|
+
|
|
77
125
|
syncSocialCookieMessages(accountId, socialCookieId, body = {}) {
|
|
78
126
|
return this.requestJson(accountPath(accountId, ["social_cookies", socialCookieId, "sync_messages"]), {
|
|
79
127
|
method: "POST",
|
|
@@ -1073,6 +1121,21 @@ export class AudientiClient {
|
|
|
1073
1121
|
return this.requestJson(accountPath(accountId, ["tools", "linkedin-review", "reports", reportId]));
|
|
1074
1122
|
}
|
|
1075
1123
|
|
|
1124
|
+
linkedinStrategyReviews(accountId, query = {}) {
|
|
1125
|
+
return this.requestJson(accountPath(accountId, ["tools", "linkedin-strategy-review", "reports"], query));
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
createLinkedinStrategyReview(accountId, body) {
|
|
1129
|
+
return this.requestJson(accountPath(accountId, ["tools", "linkedin-strategy-review", "reports"]), {
|
|
1130
|
+
method: "POST",
|
|
1131
|
+
body
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
linkedinStrategyReview(accountId, reportId) {
|
|
1136
|
+
return this.requestJson(accountPath(accountId, ["tools", "linkedin-strategy-review", "reports", reportId]));
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1076
1139
|
deleteLinkedinStrategyReview(accountId, reportId) {
|
|
1077
1140
|
return this.requestJson(accountPath(accountId, ["tools", "linkedin-strategy-review", "reports", reportId]), {
|
|
1078
1141
|
method: "DELETE"
|
package/src/cli.js
CHANGED
|
@@ -213,7 +213,16 @@ const TOOLS_LINKEDIN_REVIEW_USAGE = "Usage: audienti tools linkedin-review --url
|
|
|
213
213
|
const TOOLS_LINKEDIN_REVIEW_REPORTS_USAGE = "Usage: audienti tools linkedin-review reports [--limit <n>] [--json] [--account <acct_id>]";
|
|
214
214
|
const TOOLS_LINKEDIN_REVIEW_SHOW_USAGE = "Usage: audienti tools linkedin-review show <rprt_id> [--json] [--account <acct_id>]";
|
|
215
215
|
const TOOLS_LINKEDIN_REVIEW_STATUS_USAGE = "Usage: audienti tools linkedin-review status <rprt_id> [--json] [--account <acct_id>]";
|
|
216
|
+
const TOOLS_LINKEDIN_STRATEGY_REVIEW_LIST_USAGE = "Usage: audienti tools linkedin-strategy-review list [--limit <n>] [--json] [--account <acct_id>]";
|
|
217
|
+
const TOOLS_LINKEDIN_STRATEGY_REVIEW_CREATE_USAGE = "Usage: audienti tools linkedin-strategy-review create --url <linkedin_profile_url> [--json] [--account <acct_id>]";
|
|
218
|
+
const TOOLS_LINKEDIN_STRATEGY_REVIEW_SHOW_USAGE = "Usage: audienti tools linkedin-strategy-review show <rprt_id> [--json] [--account <acct_id>]";
|
|
216
219
|
const TOOLS_LINKEDIN_STRATEGY_REVIEW_DELETE_USAGE = "Usage: audienti tools linkedin-strategy-review delete <rprt_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
|
|
220
|
+
const TOOLS_LINKEDIN_STRATEGY_REVIEW_USAGE = [
|
|
221
|
+
TOOLS_LINKEDIN_STRATEGY_REVIEW_LIST_USAGE,
|
|
222
|
+
TOOLS_LINKEDIN_STRATEGY_REVIEW_CREATE_USAGE,
|
|
223
|
+
TOOLS_LINKEDIN_STRATEGY_REVIEW_SHOW_USAGE,
|
|
224
|
+
TOOLS_LINKEDIN_STRATEGY_REVIEW_DELETE_USAGE
|
|
225
|
+
].join("\n");
|
|
217
226
|
const LISTS_BULK_ADD_TAG_USAGE = "Usage: audienti lists bulk-add-tag --tag <tag> <list_id> [list_id...] [--json] [--account <acct_id>]";
|
|
218
227
|
const LISTS_MERGE_USAGE = "Usage: audienti lists merge <list_id> <list_id> [--json] [--account <acct_id>]";
|
|
219
228
|
const LISTS_EXPORT_USAGE = "Usage: audienti lists export <list_id> [--output <file.csv>] [--inactive-reason <reason>] [--json] [--account <acct_id>]";
|
|
@@ -4566,7 +4575,10 @@ async function toolsLinkedinReviewStatus(args, context, { accountOverride } = {}
|
|
|
4566
4575
|
|
|
4567
4576
|
async function toolsLinkedinStrategyReview(args, context, { accountOverride } = {}) {
|
|
4568
4577
|
const [subaction, ...rest] = args;
|
|
4569
|
-
if (subaction
|
|
4578
|
+
if (subaction === "list") return toolsLinkedinStrategyReviewList(rest, context, { accountOverride });
|
|
4579
|
+
if (subaction === "create") return toolsLinkedinStrategyReviewCreate(rest, context, { accountOverride });
|
|
4580
|
+
if (subaction === "show") return toolsLinkedinStrategyReviewShow(rest, context, { accountOverride });
|
|
4581
|
+
if (subaction !== "delete") throw new CommandError(TOOLS_LINKEDIN_STRATEGY_REVIEW_USAGE);
|
|
4570
4582
|
|
|
4571
4583
|
const { values, positionals } = parseCommandArgs(rest, { ...jsonOptions(), confirm: { type: "string" } });
|
|
4572
4584
|
const normalizedConfirm = String(values.confirm || "").trim().toLowerCase();
|
|
@@ -4581,6 +4593,56 @@ async function toolsLinkedinStrategyReview(args, context, { accountOverride } =
|
|
|
4581
4593
|
writeLine(context.stdout, `Deleted LinkedIn strategy review report ${display(payload?.id || positionals[0])}.`);
|
|
4582
4594
|
}
|
|
4583
4595
|
|
|
4596
|
+
async function toolsLinkedinStrategyReviewList(args, context, { accountOverride } = {}) {
|
|
4597
|
+
const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), limit: { type: "string" } });
|
|
4598
|
+
if (positionals.length > 0) throw new CommandError(TOOLS_LINKEDIN_STRATEGY_REVIEW_LIST_USAGE);
|
|
4599
|
+
|
|
4600
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
4601
|
+
const payload = await client.linkedinStrategyReviews(accountId, compactObject({ limit: values.limit }));
|
|
4602
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
4603
|
+
|
|
4604
|
+
const rows = Array.isArray(payload?.reports) ? payload.reports : [];
|
|
4605
|
+
if (rows.length === 0) {
|
|
4606
|
+
writeLine(context.stdout, "No LinkedIn strategy review reports found.");
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
writeAlignedTable(context, ["REPORT ID", "STATUS", "STAGE", "PROFILE", "UPDATED"], rows.map(linkedinReviewReportRow));
|
|
4610
|
+
writeLine(context.stdout, "");
|
|
4611
|
+
writeLine(context.stdout, "Inspect one report with `audienti tools linkedin-strategy-review show <rprt_id>`.");
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4614
|
+
async function toolsLinkedinStrategyReviewCreate(args, context, { accountOverride } = {}) {
|
|
4615
|
+
const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), url: { type: "string" } });
|
|
4616
|
+
if (positionals.length > 0 || !values.url) throw new CommandError(TOOLS_LINKEDIN_STRATEGY_REVIEW_CREATE_USAGE);
|
|
4617
|
+
|
|
4618
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
4619
|
+
const payload = await client.createLinkedinStrategyReview(accountId, { linkedin_url: values.url });
|
|
4620
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
4621
|
+
|
|
4622
|
+
renderLinkedinReviewStatus(payload, context, { title: "LinkedIn strategy review queued" });
|
|
4623
|
+
const reportId = payload?.report?.prefix_id;
|
|
4624
|
+
if (reportId) writeLine(context.stdout, `Run \`audienti tools linkedin-strategy-review show ${reportId}\` to check progress.`);
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
async function toolsLinkedinStrategyReviewShow(args, context, { accountOverride } = {}) {
|
|
4628
|
+
const { values, positionals } = parseCommandArgs(args, jsonOptions());
|
|
4629
|
+
if (positionals.length !== 1) throw new CommandError(TOOLS_LINKEDIN_STRATEGY_REVIEW_SHOW_USAGE);
|
|
4630
|
+
|
|
4631
|
+
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
4632
|
+
const payload = await client.linkedinStrategyReview(accountId, positionals[0]);
|
|
4633
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
4634
|
+
|
|
4635
|
+
renderLinkedinReviewStatus(payload, context, { title: "LinkedIn strategy review" });
|
|
4636
|
+
const content = payload?.content?.payload || {};
|
|
4637
|
+
if (Object.keys(content).length === 0) {
|
|
4638
|
+
writeLine(context.stdout, "");
|
|
4639
|
+
writeLine(context.stdout, "No report content is available yet.");
|
|
4640
|
+
return;
|
|
4641
|
+
}
|
|
4642
|
+
writeLine(context.stdout, "");
|
|
4643
|
+
writeLine(context.stdout, "Use --json for the full report content.");
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4584
4646
|
async function operatorQueue(args, context, { accountOverride } = {}) {
|
|
4585
4647
|
const { values, positionals } = parseCommandArgs(args, operatorFilterOptions(operatorPaginationOptions()));
|
|
4586
4648
|
if (positionals.length > 0) throw new CommandError("Usage: audienti operator queue [--json] [filters] [--account <acct_id>]");
|
|
@@ -11659,6 +11721,48 @@ const HELP_TOPICS = new Map([
|
|
|
11659
11721
|
" PATCH /api/v1/accounts/:account_id/tasks/bulk_update.json"
|
|
11660
11722
|
].join("\n")],
|
|
11661
11723
|
|
|
11724
|
+
["tools linkedin-strategy-review list", [
|
|
11725
|
+
"Usage:",
|
|
11726
|
+
` ${TOOLS_LINKEDIN_STRATEGY_REVIEW_LIST_USAGE.slice("Usage: ".length)}`,
|
|
11727
|
+
"",
|
|
11728
|
+
"Status: implemented",
|
|
11729
|
+
"",
|
|
11730
|
+
"Purpose:",
|
|
11731
|
+
" List recent LinkedIn strategy review reports for the active account, newest first.",
|
|
11732
|
+
"",
|
|
11733
|
+
"API:",
|
|
11734
|
+
" GET /api/v1/accounts/:account_id/tools/linkedin-strategy-review/reports.json"
|
|
11735
|
+
].join("\n")],
|
|
11736
|
+
|
|
11737
|
+
["tools linkedin-strategy-review create", [
|
|
11738
|
+
"Usage:",
|
|
11739
|
+
` ${TOOLS_LINKEDIN_STRATEGY_REVIEW_CREATE_USAGE.slice("Usage: ".length)}`,
|
|
11740
|
+
"",
|
|
11741
|
+
"Status: implemented",
|
|
11742
|
+
"",
|
|
11743
|
+
"Purpose:",
|
|
11744
|
+
" Queue a LinkedIn strategy review for a person profile, like the Tools page.",
|
|
11745
|
+
"",
|
|
11746
|
+
"Input shape:",
|
|
11747
|
+
" url: LinkedIn person profile URL (post and company URLs are refused)",
|
|
11748
|
+
"",
|
|
11749
|
+
"API:",
|
|
11750
|
+
" POST /api/v1/accounts/:account_id/tools/linkedin-strategy-review/reports.json"
|
|
11751
|
+
].join("\n")],
|
|
11752
|
+
|
|
11753
|
+
["tools linkedin-strategy-review show", [
|
|
11754
|
+
"Usage:",
|
|
11755
|
+
` ${TOOLS_LINKEDIN_STRATEGY_REVIEW_SHOW_USAGE.slice("Usage: ".length)}`,
|
|
11756
|
+
"",
|
|
11757
|
+
"Status: implemented",
|
|
11758
|
+
"",
|
|
11759
|
+
"Purpose:",
|
|
11760
|
+
" Show one LinkedIn strategy review's status; --json includes the report content.",
|
|
11761
|
+
"",
|
|
11762
|
+
"API:",
|
|
11763
|
+
" GET /api/v1/accounts/:account_id/tools/linkedin-strategy-review/reports/:id.json"
|
|
11764
|
+
].join("\n")],
|
|
11765
|
+
|
|
11662
11766
|
["tools linkedin-strategy-review delete", [
|
|
11663
11767
|
"Usage:",
|
|
11664
11768
|
` ${TOOLS_LINKEDIN_STRATEGY_REVIEW_DELETE_USAGE.slice("Usage: ".length)}`,
|
|
@@ -13137,6 +13241,9 @@ const HELP_TOPICS = new Map([
|
|
|
13137
13241
|
" audienti tools linkedin-review reports [--limit <n>] [--json]",
|
|
13138
13242
|
" audienti tools linkedin-review show <rprt_id> [--json]",
|
|
13139
13243
|
" audienti tools linkedin-review status <rprt_id> [--json]",
|
|
13244
|
+
" audienti tools linkedin-strategy-review list [--limit <n>] [--json]",
|
|
13245
|
+
" audienti tools linkedin-strategy-review create --url <linkedin_profile_url> [--json]",
|
|
13246
|
+
" audienti tools linkedin-strategy-review show <rprt_id> [--json]",
|
|
13140
13247
|
" audienti tools linkedin-strategy-review delete <rprt_id> --confirm <yes|true|Y|y> [--json]",
|
|
13141
13248
|
"",
|
|
13142
13249
|
"Status: implemented",
|
|
@@ -13149,6 +13256,9 @@ const HELP_TOPICS = new Map([
|
|
|
13149
13256
|
" audienti tools linkedin-review reports List recent LinkedIn Review reports for the active account.",
|
|
13150
13257
|
" audienti tools linkedin-review show View the completed report content in the terminal.",
|
|
13151
13258
|
" audienti tools linkedin-review status Show the current report stage and run status.",
|
|
13259
|
+
" audienti tools linkedin-strategy-review list List recent LinkedIn strategy review reports.",
|
|
13260
|
+
" audienti tools linkedin-strategy-review create Queue a LinkedIn strategy review for a person profile.",
|
|
13261
|
+
" audienti tools linkedin-strategy-review show Show one strategy review's status and content.",
|
|
13152
13262
|
" audienti tools linkedin-strategy-review delete Delete one LinkedIn strategy review report."
|
|
13153
13263
|
].join("\n")],
|
|
13154
13264
|
|
|
@@ -14070,6 +14180,195 @@ const HELP_TOPICS = new Map([
|
|
|
14070
14180
|
].join("\n")]
|
|
14071
14181
|
]);
|
|
14072
14182
|
|
|
14183
|
+
// Social account credentials (#2306). Secrets never travel as argv flags,
|
|
14184
|
+
// because argv lands in shell history and the process list. They come from
|
|
14185
|
+
// stdin, one named environment variable, or a no-echo terminal prompt, and the
|
|
14186
|
+
// CLI never prints them.
|
|
14187
|
+
const SOCIAL_SECRET_KEYS = ["password", "otp_secret", "cookie_bundle", "messaging_pin", "otp_code"];
|
|
14188
|
+
const SOCIAL_SECRET_FLAGS = new Set(["password", "pass", "otp", "otp-code", "code", "otp-secret", "totp", "totp-secret", "cookie", "cookies", "cookie-bundle", "messaging-pin", "pin", "secret"]);
|
|
14189
|
+
const SOCIAL_PASSWORD_ENV = "AUDIENTI_SOCIAL_PASSWORD";
|
|
14190
|
+
const SOCIAL_OTP_ENV = "AUDIENTI_OTP_CODE";
|
|
14191
|
+
const SOCIAL_COOKIE_ATTRIBUTE_KEYS = new Set([
|
|
14192
|
+
"service_identifier", "username", "name", "email", "phone_number",
|
|
14193
|
+
"geo_location", "country_code", "postal_code", "state_code", "city",
|
|
14194
|
+
"imap_server", "imap_port", "smtp_server", "smtp_port", "use_ssl",
|
|
14195
|
+
"automation_controls", "rate_limit_overrides", "operator_controls"
|
|
14196
|
+
]);
|
|
14197
|
+
const SOCIAL_COOKIE_FIELD_OPTIONS = {
|
|
14198
|
+
name: { type: "string" },
|
|
14199
|
+
email: { type: "string" },
|
|
14200
|
+
attributes: { type: "string" },
|
|
14201
|
+
"password-stdin": { type: "boolean" },
|
|
14202
|
+
"secrets-stdin": { type: "boolean" },
|
|
14203
|
+
"password-env": { type: "boolean" },
|
|
14204
|
+
"prompt-password": { type: "boolean" }
|
|
14205
|
+
};
|
|
14206
|
+
|
|
14207
|
+
function rejectSecretArgs(args) {
|
|
14208
|
+
for (const arg of args) {
|
|
14209
|
+
if (!String(arg).startsWith("--")) continue;
|
|
14210
|
+
const flag = String(arg).slice(2).split("=")[0];
|
|
14211
|
+
if (SOCIAL_SECRET_FLAGS.has(flag)) {
|
|
14212
|
+
throw new CommandError(`--${flag} is not accepted because command-line secrets leak into shell history. Use --password-stdin, --secrets-stdin, --otp-stdin, the ${SOCIAL_PASSWORD_ENV} or ${SOCIAL_OTP_ENV} variable, or the hidden prompt.`);
|
|
14213
|
+
}
|
|
14214
|
+
}
|
|
14215
|
+
}
|
|
14216
|
+
|
|
14217
|
+
function rejectSecretKeys(object, flag) {
|
|
14218
|
+
const walk = (value) => {
|
|
14219
|
+
if (!value || typeof value !== "object") return;
|
|
14220
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
14221
|
+
if (SOCIAL_SECRET_KEYS.includes(key)) throw new CommandError(`${flag} must not contain ${key}. Send secrets through stdin, an environment variable, or the hidden prompt.`);
|
|
14222
|
+
walk(nested);
|
|
14223
|
+
}
|
|
14224
|
+
};
|
|
14225
|
+
walk(object);
|
|
14226
|
+
}
|
|
14227
|
+
|
|
14228
|
+
function trimSecret(text) {
|
|
14229
|
+
return String(text ?? "").replace(/\r?\n$/, "");
|
|
14230
|
+
}
|
|
14231
|
+
|
|
14232
|
+
// Reads one line from the terminal without echoing it.
|
|
14233
|
+
async function promptHidden(context, question) {
|
|
14234
|
+
const input = context.stdin;
|
|
14235
|
+
if (!input?.isTTY || typeof input.setRawMode !== "function") {
|
|
14236
|
+
throw new CommandError("No terminal is available for a hidden prompt. Use stdin or an environment variable instead.");
|
|
14237
|
+
}
|
|
14238
|
+
const output = context.stderr || context.stdout;
|
|
14239
|
+
output.write(question);
|
|
14240
|
+
input.setRawMode(true);
|
|
14241
|
+
input.resume();
|
|
14242
|
+
|
|
14243
|
+
return new Promise((resolve, reject) => {
|
|
14244
|
+
let value = "";
|
|
14245
|
+
const finish = (error) => {
|
|
14246
|
+
input.removeListener("data", onData);
|
|
14247
|
+
input.setRawMode(false);
|
|
14248
|
+
input.pause();
|
|
14249
|
+
output.write("\n");
|
|
14250
|
+
if (error) reject(error);
|
|
14251
|
+
else resolve(value);
|
|
14252
|
+
};
|
|
14253
|
+
const onData = (chunk) => {
|
|
14254
|
+
for (const char of String(chunk)) {
|
|
14255
|
+
if (char === "\r" || char === "\n" || char === "\u0004") return finish();
|
|
14256
|
+
if (char === "\u0003") return finish(new CommandError("Cancelled."));
|
|
14257
|
+
if (char === "\u007f" || char === "\b") value = value.slice(0, -1);
|
|
14258
|
+
else value += char;
|
|
14259
|
+
}
|
|
14260
|
+
};
|
|
14261
|
+
input.on("data", onData);
|
|
14262
|
+
});
|
|
14263
|
+
}
|
|
14264
|
+
|
|
14265
|
+
async function readSocialOtp(values, usageLine, context) {
|
|
14266
|
+
if (values["otp-stdin"] && values["otp-env"]) throw new CommandError(usageLine);
|
|
14267
|
+
let code;
|
|
14268
|
+
if (values["otp-stdin"]) code = await readStdinText(context);
|
|
14269
|
+
else if (values["otp-env"]) code = context.env?.[SOCIAL_OTP_ENV];
|
|
14270
|
+
else if (stdinIsInteractive(context)) code = await promptHidden(context, "One-time code (hidden): ");
|
|
14271
|
+
else throw new CommandError(`Provide the code with --otp-stdin, --otp-env (${SOCIAL_OTP_ENV}), or run in a terminal for a hidden prompt.`);
|
|
14272
|
+
|
|
14273
|
+
const trimmed = String(code ?? "").trim();
|
|
14274
|
+
if (!trimmed) throw new CommandError("The one-time code is empty.");
|
|
14275
|
+
return trimmed;
|
|
14276
|
+
}
|
|
14277
|
+
|
|
14278
|
+
// Returns the secret fields to send, or {} when none were requested.
|
|
14279
|
+
async function readSocialSecrets(values, usageLine, context, { allowBundle = true } = {}) {
|
|
14280
|
+
const sources = ["password-stdin", "secrets-stdin", "password-env", "prompt-password"].filter((key) => values[key]);
|
|
14281
|
+
if (sources.length > 1) throw new CommandError(`Use only one of --${sources.join(", --")}.`);
|
|
14282
|
+
const [source] = sources;
|
|
14283
|
+
if (!source) return {};
|
|
14284
|
+
|
|
14285
|
+
if (source === "password-stdin") return { password: trimSecret(await readStdinText(context)) };
|
|
14286
|
+
if (source === "password-env") {
|
|
14287
|
+
const password = context.env?.[SOCIAL_PASSWORD_ENV];
|
|
14288
|
+
if (!password) throw new CommandError(`${SOCIAL_PASSWORD_ENV} is not set.`);
|
|
14289
|
+
return { password };
|
|
14290
|
+
}
|
|
14291
|
+
if (source === "prompt-password") return { password: await promptHidden(context, "Password (hidden): ") };
|
|
14292
|
+
|
|
14293
|
+
if (!allowBundle) throw new CommandError(usageLine);
|
|
14294
|
+
let parsed;
|
|
14295
|
+
try {
|
|
14296
|
+
parsed = JSON.parse(await readStdinText(context));
|
|
14297
|
+
} catch {
|
|
14298
|
+
throw new CommandError("--secrets-stdin expects a JSON object with password, otp_secret, cookie_bundle, or messaging_pin.");
|
|
14299
|
+
}
|
|
14300
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14301
|
+
throw new CommandError("--secrets-stdin expects a JSON object with password, otp_secret, cookie_bundle, or messaging_pin.");
|
|
14302
|
+
}
|
|
14303
|
+
const allowed = ["password", "otp_secret", "cookie_bundle", "messaging_pin"];
|
|
14304
|
+
const unknown = Object.keys(parsed).filter((key) => !allowed.includes(key));
|
|
14305
|
+
if (unknown.length > 0) throw new CommandError(`--secrets-stdin accepts only ${allowed.join(", ")}.`);
|
|
14306
|
+
return parsed;
|
|
14307
|
+
}
|
|
14308
|
+
|
|
14309
|
+
async function socialCookieBody(values, usageLine, context, fields) {
|
|
14310
|
+
const attributes = parseJsonObjectOption(values.attributes, "--attributes") || {};
|
|
14311
|
+
const unknown = Object.keys(attributes).filter((key) => !SOCIAL_COOKIE_ATTRIBUTE_KEYS.has(key));
|
|
14312
|
+
if (unknown.length > 0) {
|
|
14313
|
+
throw new CommandError(`--attributes accepts only ${[...SOCIAL_COOKIE_ATTRIBUTE_KEYS].join(", ")}. Secrets go through stdin, an environment variable, or the hidden prompt.`);
|
|
14314
|
+
}
|
|
14315
|
+
rejectSecretKeys(attributes, "--attributes");
|
|
14316
|
+
|
|
14317
|
+
return {
|
|
14318
|
+
...attributes,
|
|
14319
|
+
...compactObject({ ...fields, name: values.name, email: values.email }),
|
|
14320
|
+
...(await readSocialSecrets(values, usageLine, context))
|
|
14321
|
+
};
|
|
14322
|
+
}
|
|
14323
|
+
|
|
14324
|
+
function socialCookieRightsBody(values, usageLine) {
|
|
14325
|
+
if (values.workspaces !== undefined) {
|
|
14326
|
+
if (values.workspace || values.grant || values.revoke) throw new CommandError(usageLine);
|
|
14327
|
+
return { access_account_ids: values.workspaces.split(",").map((id) => id.trim()).filter(Boolean) };
|
|
14328
|
+
}
|
|
14329
|
+
if (!values.workspace || values.grant === values.revoke) throw new CommandError(usageLine);
|
|
14330
|
+
return { access_account_id: values.workspace, granted: Boolean(values.grant) };
|
|
14331
|
+
}
|
|
14332
|
+
|
|
14333
|
+
async function socialCookieSettingsBody(values, usageLine, context) {
|
|
14334
|
+
const body = parseJsonObjectOption(values.body, "--body") || {};
|
|
14335
|
+
rejectSecretKeys(body, "--body");
|
|
14336
|
+
const merged = {
|
|
14337
|
+
...body,
|
|
14338
|
+
...compactObject({
|
|
14339
|
+
scope: values.scope,
|
|
14340
|
+
enabled: values.enabled,
|
|
14341
|
+
key: values.key,
|
|
14342
|
+
target: values.target,
|
|
14343
|
+
daily: values.daily,
|
|
14344
|
+
hourly: values.hourly,
|
|
14345
|
+
action_type: values["action-type"],
|
|
14346
|
+
mode: values.mode,
|
|
14347
|
+
level: values.level
|
|
14348
|
+
})
|
|
14349
|
+
};
|
|
14350
|
+
|
|
14351
|
+
const secrets = await readSocialSecrets(values, usageLine, context, { allowBundle: false });
|
|
14352
|
+
if (Object.keys(secrets).length > 0) {
|
|
14353
|
+
if (!["details", "servers"].includes(values.scope)) throw new CommandError("A password is accepted only with --scope details or --scope servers.");
|
|
14354
|
+
merged.social_cookie = { ...(merged.social_cookie || {}), ...secrets };
|
|
14355
|
+
}
|
|
14356
|
+
return merged;
|
|
14357
|
+
}
|
|
14358
|
+
|
|
14359
|
+
function describeSocialCookie(cookie) {
|
|
14360
|
+
if (!cookie) return "Social account not found.";
|
|
14361
|
+
const lines = [
|
|
14362
|
+
`${display(cookie.name || cookie.username)} (${display(cookie.prefix_id)})`,
|
|
14363
|
+
`Platform: ${display(cookie.platform)} | Status: ${display(cookie.status)}${cookie.autopilot_paused ? " | Autopilot paused" : ""}`,
|
|
14364
|
+
`Connection: ${display(cookie.connection?.title)}`
|
|
14365
|
+
];
|
|
14366
|
+
if (cookie.connection?.body) lines.push(` ${cookie.connection.body}`);
|
|
14367
|
+
if (cookie.otp_pending) lines.push("Waiting for a one-time code: run `audienti social-cookies submit-otp`.");
|
|
14368
|
+
for (const fact of cookie.account_facts || []) lines.push(`${display(fact?.label)}: ${display(fact?.value)}`);
|
|
14369
|
+
return lines.join("\n");
|
|
14370
|
+
}
|
|
14371
|
+
|
|
14073
14372
|
// Single-purpose actions that mirror one web button each (#2300, #2302).
|
|
14074
14373
|
// Every entry calls one API route that shares its service with the web action.
|
|
14075
14374
|
const ACCOUNT_OPTION_USAGE = "[--json] [--account <acct_id>]";
|
|
@@ -14427,6 +14726,131 @@ const PARITY_ACTION_COMMANDS = new Map([
|
|
|
14427
14726
|
required: ["premise"],
|
|
14428
14727
|
run: (client, accountId, [id], values) => client.updateMotionPremise(accountId, id, { motion: { premise: values.premise } }),
|
|
14429
14728
|
done: ([id]) => `Updated the premise for ${id}.`
|
|
14729
|
+
}],
|
|
14730
|
+
// Social account page (#2306). Passwords, OTP codes, cookies, TOTP secrets
|
|
14731
|
+
// and the messaging PIN are never argv flags: they come from stdin, an
|
|
14732
|
+
// environment variable, or a no-echo terminal prompt, and are never printed.
|
|
14733
|
+
["social-cookies show", {
|
|
14734
|
+
usage: "audienti social-cookies show <scok_id>",
|
|
14735
|
+
purpose: "Show one social account's connection state and facts, like its Overview. Never shows credentials.",
|
|
14736
|
+
api: "GET /api/v1/accounts/:account_id/social_cookies/:id.json",
|
|
14737
|
+
run: (client, accountId, [id]) => client.socialCookie(accountId, id),
|
|
14738
|
+
done: (_positionals, payload) => describeSocialCookie(payload?.social_cookie)
|
|
14739
|
+
}],
|
|
14740
|
+
["social-cookies pause", {
|
|
14741
|
+
usage: "audienti social-cookies pause <scok_id>",
|
|
14742
|
+
purpose: "Pause one social account so no activity runs until it is resumed.",
|
|
14743
|
+
api: "POST /api/v1/accounts/:account_id/social_cookies/:id/pause.json",
|
|
14744
|
+
run: (client, accountId, [id]) => client.pauseSocialCookie(accountId, id),
|
|
14745
|
+
done: (_positionals, payload) => display(payload?.message)
|
|
14746
|
+
}],
|
|
14747
|
+
["social-cookies resume", {
|
|
14748
|
+
usage: "audienti social-cookies resume <scok_id>",
|
|
14749
|
+
purpose: "Resume a paused social account; it reconnects before activity restarts.",
|
|
14750
|
+
api: "POST /api/v1/accounts/:account_id/social_cookies/:id/resume.json",
|
|
14751
|
+
run: (client, accountId, [id]) => client.resumeSocialCookie(accountId, id),
|
|
14752
|
+
done: (_positionals, payload) => display(payload?.message)
|
|
14753
|
+
}],
|
|
14754
|
+
["social-cookies resume-autopilot", {
|
|
14755
|
+
usage: "audienti social-cookies resume-autopilot <scok_id>",
|
|
14756
|
+
purpose: "Clear an automatic autopilot pause on one social account.",
|
|
14757
|
+
api: "POST /api/v1/accounts/:account_id/social_cookies/:id/resume_autopilot.json",
|
|
14758
|
+
run: (client, accountId, [id]) => client.resumeSocialCookieAutopilot(accountId, id),
|
|
14759
|
+
done: (_positionals, payload) => display(payload?.message)
|
|
14760
|
+
}],
|
|
14761
|
+
["social-cookies recheck-account-type", {
|
|
14762
|
+
usage: "audienti social-cookies recheck-account-type <scok_id>",
|
|
14763
|
+
purpose: "Queue a LinkedIn account-type check (Premium, Sales Navigator) for one connected account.",
|
|
14764
|
+
api: "POST /api/v1/accounts/:account_id/social_cookies/:id/recheck_account_type.json",
|
|
14765
|
+
run: (client, accountId, [id]) => client.recheckSocialCookieAccountType(accountId, id),
|
|
14766
|
+
done: (_positionals, payload) => display(payload?.message)
|
|
14767
|
+
}],
|
|
14768
|
+
["social-cookies reconnect", {
|
|
14769
|
+
usage: "audienti social-cookies reconnect <scok_id>",
|
|
14770
|
+
purpose: "Ask one social account to reconnect; a mailbox rechecks its connection.",
|
|
14771
|
+
api: "POST /api/v1/accounts/:account_id/social_cookies/:id/reconnect.json",
|
|
14772
|
+
run: (client, accountId, [id]) => client.reconnectSocialCookie(accountId, id),
|
|
14773
|
+
done: (_positionals, payload) => display(payload?.message)
|
|
14774
|
+
}],
|
|
14775
|
+
["social-cookies delete", {
|
|
14776
|
+
usage: "audienti social-cookies delete <scok_id>",
|
|
14777
|
+
purpose: "Delete one social account you own.",
|
|
14778
|
+
api: "DELETE /api/v1/accounts/:account_id/social_cookies/:id.json",
|
|
14779
|
+
run: (client, accountId, [id]) => client.deleteSocialCookie(accountId, id),
|
|
14780
|
+
done: (_positionals, payload) => display(payload?.message)
|
|
14781
|
+
}],
|
|
14782
|
+
["social-cookies submit-otp", {
|
|
14783
|
+
usage: "audienti social-cookies submit-otp <scok_id> [--otp-stdin | --otp-env]",
|
|
14784
|
+
purpose: "Send the one-time login code a social account is waiting for. Reads the code from stdin (--otp-stdin), AUDIENTI_OTP_CODE (--otp-env), or a hidden prompt.",
|
|
14785
|
+
api: "POST /api/v1/accounts/:account_id/social_cookies/:id/submit_otp.json",
|
|
14786
|
+
secretInput: true,
|
|
14787
|
+
options: { "otp-stdin": { type: "boolean" }, "otp-env": { type: "boolean" } },
|
|
14788
|
+
parse: async (values, usageLine, context) => ({ ...values, otpCode: await readSocialOtp(values, usageLine, context) }),
|
|
14789
|
+
run: (client, accountId, [id], values) => client.submitSocialCookieOtp(accountId, id, { otp_code: values.otpCode }),
|
|
14790
|
+
done: (_positionals, payload) => display(payload?.message)
|
|
14791
|
+
}],
|
|
14792
|
+
["social-cookies create", {
|
|
14793
|
+
usage: "audienti social-cookies create --service <linkedin|gmail|...> --username <name> [--name <label>] [--email <email>] [--attributes <json>] [--password-stdin | --secrets-stdin | --password-env | --prompt-password]",
|
|
14794
|
+
minPositionals: 0,
|
|
14795
|
+
maxPositionals: 0,
|
|
14796
|
+
purpose: "Add a social account and queue its login, like Add account on the web. Secrets come from stdin, AUDIENTI_SOCIAL_PASSWORD, or a hidden prompt; never from flags.",
|
|
14797
|
+
api: "POST /api/v1/accounts/:account_id/social_cookies.json",
|
|
14798
|
+
secretInput: true,
|
|
14799
|
+
options: { service: { type: "string" }, username: { type: "string" }, ...SOCIAL_COOKIE_FIELD_OPTIONS },
|
|
14800
|
+
required: ["service"],
|
|
14801
|
+
parse: async (values, usageLine, context) => ({
|
|
14802
|
+
...values,
|
|
14803
|
+
body: { social_cookie: await socialCookieBody(values, usageLine, context, { service_identifier: values.service, username: values.username }) }
|
|
14804
|
+
}),
|
|
14805
|
+
run: (client, accountId, _positionals, values) => client.createSocialCookie(accountId, values.body),
|
|
14806
|
+
done: (_positionals, payload) => `Added social account ${display(payload?.social_cookie?.prefix_id)}; login is queued.`
|
|
14807
|
+
}],
|
|
14808
|
+
["social-cookies update", {
|
|
14809
|
+
usage: "audienti social-cookies update <scok_id> [--username <name>] [--name <label>] [--email <email>] [--attributes <json>] [--password-stdin | --secrets-stdin | --password-env | --prompt-password]",
|
|
14810
|
+
purpose: "Edit one social account's details, controls and limits, like its edit page. A stored password is kept unless a new one is sent.",
|
|
14811
|
+
api: "PATCH /api/v1/accounts/:account_id/social_cookies/:id.json",
|
|
14812
|
+
secretInput: true,
|
|
14813
|
+
options: { username: { type: "string" }, ...SOCIAL_COOKIE_FIELD_OPTIONS },
|
|
14814
|
+
parse: async (values, usageLine, context) => ({
|
|
14815
|
+
...values,
|
|
14816
|
+
body: { social_cookie: await socialCookieBody(values, usageLine, context, { username: values.username }) }
|
|
14817
|
+
}),
|
|
14818
|
+
run: (client, accountId, [id], values) => client.updateSocialCookie(accountId, id, values.body),
|
|
14819
|
+
done: ([id]) => `Updated social account ${id}.`
|
|
14820
|
+
}],
|
|
14821
|
+
["social-cookies rights", {
|
|
14822
|
+
usage: "audienti social-cookies rights <scok_id> (--workspace <acct_id> (--grant | --revoke) | --workspaces <acct_id,acct_id>)",
|
|
14823
|
+
purpose: "Choose which of your workspaces may use one social account, like its Access tab.",
|
|
14824
|
+
api: "PATCH /api/v1/accounts/:account_id/social_cookies/:id/rights.json",
|
|
14825
|
+
options: { workspace: { type: "string" }, workspaces: { type: "string" }, grant: { type: "boolean" }, revoke: { type: "boolean" } },
|
|
14826
|
+
parse: (values, usageLine) => ({ ...values, body: socialCookieRightsBody(values, usageLine) }),
|
|
14827
|
+
run: (client, accountId, [id], values) => client.updateSocialCookieRights(accountId, id, values.body),
|
|
14828
|
+
done: (_positionals, payload) => `Workspaces: ${(payload?.accounts || []).map((account) => `${display(account?.name)} (${display(account?.prefix_id)})`).join(", ") || "none"}.`
|
|
14829
|
+
}],
|
|
14830
|
+
["social-cookies settings", {
|
|
14831
|
+
usage: "audienti social-cookies settings <scok_id> --scope <scope> [--enabled <true|false>] [--key <key>] [--target <n>] [--daily <n>] [--hourly <n>] [--action-type <type>] [--mode <mode>] [--level <level>] [--body <json>] [--password-stdin | --password-env | --prompt-password]",
|
|
14832
|
+
purpose: "Change one Overview setting (scopes: sending, gate, action, visibility_mode, operator, limits, limits_preset, hours, advanced, automation, details, location, servers). Only details and servers accept a password, from stdin, AUDIENTI_SOCIAL_PASSWORD, or a hidden prompt.",
|
|
14833
|
+
api: "PATCH /api/v1/accounts/:account_id/social_cookies/:id/settings.json",
|
|
14834
|
+
secretInput: true,
|
|
14835
|
+
options: {
|
|
14836
|
+
scope: { type: "string" },
|
|
14837
|
+
enabled: { type: "string" },
|
|
14838
|
+
key: { type: "string" },
|
|
14839
|
+
target: { type: "string" },
|
|
14840
|
+
daily: { type: "string" },
|
|
14841
|
+
hourly: { type: "string" },
|
|
14842
|
+
"action-type": { type: "string" },
|
|
14843
|
+
mode: { type: "string" },
|
|
14844
|
+
level: { type: "string" },
|
|
14845
|
+
body: { type: "string" },
|
|
14846
|
+
"password-stdin": { type: "boolean" },
|
|
14847
|
+
"password-env": { type: "boolean" },
|
|
14848
|
+
"prompt-password": { type: "boolean" }
|
|
14849
|
+
},
|
|
14850
|
+
required: ["scope"],
|
|
14851
|
+
parse: async (values, usageLine, context) => ({ ...values, body: await socialCookieSettingsBody(values, usageLine, context) }),
|
|
14852
|
+
run: (client, accountId, [id], values) => client.updateSocialCookieSettings(accountId, id, values.body),
|
|
14853
|
+
done: ([id], payload) => `Saved ${display(payload?.scope)} for social account ${id}.`
|
|
14430
14854
|
}]
|
|
14431
14855
|
]);
|
|
14432
14856
|
|
|
@@ -14476,13 +14900,14 @@ function parseJsonObjectOption(raw, flag) {
|
|
|
14476
14900
|
async function runParityActionCommand(topic, args, context, { accountOverride } = {}) {
|
|
14477
14901
|
const spec = PARITY_ACTION_COMMANDS.get(topic);
|
|
14478
14902
|
const usageLine = `Usage: ${spec.usage} ${ACCOUNT_OPTION_USAGE}`;
|
|
14903
|
+
if (spec.secretInput) rejectSecretArgs(args);
|
|
14479
14904
|
const { values, positionals } = parseCommandArgs(args, { ...jsonOptions(), ...(spec.options || {}) });
|
|
14480
14905
|
const minPositionals = spec.minPositionals ?? 1;
|
|
14481
14906
|
const maxPositionals = spec.maxPositionals ?? 1;
|
|
14482
14907
|
if (positionals.length < minPositionals || positionals.length > maxPositionals) throw new CommandError(usageLine);
|
|
14483
14908
|
if ((spec.required || []).some((key) => !values[key])) throw new CommandError(usageLine);
|
|
14484
14909
|
|
|
14485
|
-
const parsedValues = spec.parse ? spec.parse(values, usageLine) : values;
|
|
14910
|
+
const parsedValues = spec.parse ? await spec.parse(values, usageLine, context) : values;
|
|
14486
14911
|
|
|
14487
14912
|
const { client, accountId } = await requireAccountContext(context, { accountOverride });
|
|
14488
14913
|
const payload = await spec.run(client, accountId, positionals, parsedValues);
|