@oxygen-agent/cli 1.1014.136 → 1.1014.619
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/README.md +1 -1
- package/dist/admin-primary-providers-render.js +27 -19
- package/dist/cli-output.d.ts +12 -0
- package/dist/cli-output.js +395 -0
- package/dist/cli-paths.d.ts +1 -0
- package/dist/cli-paths.js +3 -0
- package/dist/cli-profile-context.d.ts +3 -0
- package/dist/cli-profile-context.js +13 -0
- package/dist/command-manifest.d.ts +15 -0
- package/dist/command-manifest.js +23 -1
- package/dist/command-support.d.ts +438 -0
- package/dist/command-support.js +1610 -0
- package/dist/commands/action-column.d.ts +3 -0
- package/dist/commands/action-column.js +43 -0
- package/dist/commands/activation.d.ts +3 -0
- package/dist/commands/activation.js +24 -0
- package/dist/commands/admin.d.ts +3 -0
- package/dist/commands/admin.js +347 -0
- package/dist/commands/ads.d.ts +3 -0
- package/dist/commands/ads.js +62 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +274 -0
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.js +77 -0
- package/dist/commands/approvals.d.ts +3 -0
- package/dist/commands/approvals.js +518 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +230 -0
- package/dist/commands/billing.d.ts +3 -0
- package/dist/commands/billing.js +357 -0
- package/dist/commands/blueprints.d.ts +3 -0
- package/dist/commands/blueprints.js +312 -0
- package/dist/commands/budget.d.ts +3 -0
- package/dist/commands/budget.js +55 -0
- package/dist/commands/callables.d.ts +3 -0
- package/dist/commands/callables.js +111 -0
- package/dist/commands/capabilities.d.ts +3 -0
- package/dist/commands/capabilities.js +44 -0
- package/dist/commands/cells.d.ts +3 -0
- package/dist/commands/cells.js +94 -0
- package/dist/commands/columns.d.ts +3 -0
- package/dist/commands/columns.js +1628 -0
- package/dist/commands/commands.d.ts +3 -0
- package/dist/commands/commands.js +45 -0
- package/dist/commands/comments.d.ts +3 -0
- package/dist/commands/comments.js +188 -0
- package/dist/commands/companies.d.ts +3 -0
- package/dist/commands/companies.js +312 -0
- package/dist/commands/connections.d.ts +3 -0
- package/dist/commands/connections.js +39 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +170 -0
- package/dist/commands/copilot.d.ts +3 -0
- package/dist/commands/copilot.js +565 -0
- package/dist/commands/crm.d.ts +3 -0
- package/dist/commands/crm.js +1672 -0
- package/dist/commands/custom-integrations.d.ts +3 -0
- package/dist/commands/custom-integrations.js +140 -0
- package/dist/commands/dashboards.d.ts +3 -0
- package/dist/commands/dashboards.js +240 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +190 -0
- package/dist/commands/deliverability.d.ts +3 -0
- package/dist/commands/deliverability.js +90 -0
- package/dist/commands/directory.d.ts +3 -0
- package/dist/commands/directory.js +101 -0
- package/dist/commands/domains.d.ts +3 -0
- package/dist/commands/domains.js +604 -0
- package/dist/commands/egress.d.ts +3 -0
- package/dist/commands/egress.js +130 -0
- package/dist/commands/email.d.ts +3 -0
- package/dist/commands/email.js +49 -0
- package/dist/commands/engagement.d.ts +3 -0
- package/dist/commands/engagement.js +189 -0
- package/dist/commands/enrich-column.d.ts +3 -0
- package/dist/commands/enrich-column.js +149 -0
- package/dist/commands/enrichment.d.ts +3 -0
- package/dist/commands/enrichment.js +49 -0
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/feeds.d.ts +3 -0
- package/dist/commands/feeds.js +486 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.js +159 -0
- package/dist/commands/followers.d.ts +3 -0
- package/dist/commands/followers.js +39 -0
- package/dist/commands/formulas.d.ts +3 -0
- package/dist/commands/formulas.js +43 -0
- package/dist/commands/home.d.ts +3 -0
- package/dist/commands/home.js +22 -0
- package/dist/commands/inbox.d.ts +3 -0
- package/dist/commands/inbox.js +779 -0
- package/dist/commands/integrations.d.ts +3 -0
- package/dist/commands/integrations.js +290 -0
- package/dist/commands/knowledge.d.ts +3 -0
- package/dist/commands/knowledge.js +874 -0
- package/dist/commands/lead-sourcing.d.ts +3 -0
- package/dist/commands/lead-sourcing.js +52 -0
- package/dist/commands/limits.d.ts +3 -0
- package/dist/commands/limits.js +54 -0
- package/dist/commands/linkedin.d.ts +3 -0
- package/dist/commands/linkedin.js +275 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +347 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mailboxes.d.ts +3 -0
- package/dist/commands/mailboxes.js +1290 -0
- package/dist/commands/managed-inboxes.d.ts +3 -0
- package/dist/commands/managed-inboxes.js +361 -0
- package/dist/commands/messages.d.ts +3 -0
- package/dist/commands/messages.js +78 -0
- package/dist/commands/notetaker.d.ts +3 -0
- package/dist/commands/notetaker.js +79 -0
- package/dist/commands/observability.d.ts +3 -0
- package/dist/commands/observability.js +125 -0
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.js +237 -0
- package/dist/commands/people.d.ts +3 -0
- package/dist/commands/people.js +353 -0
- package/dist/commands/posts.d.ts +3 -0
- package/dist/commands/posts.js +139 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +250 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +94 -0
- package/dist/commands/prompts.d.ts +3 -0
- package/dist/commands/prompts.js +84 -0
- package/dist/commands/publishing.d.ts +3 -0
- package/dist/commands/publishing.js +1419 -0
- package/dist/commands/recipes.d.ts +3 -0
- package/dist/commands/recipes.js +120 -0
- package/dist/commands/reviews.d.ts +3 -0
- package/dist/commands/reviews.js +75 -0
- package/dist/commands/rows.d.ts +3 -0
- package/dist/commands/rows.js +27 -0
- package/dist/commands/runs.d.ts +3 -0
- package/dist/commands/runs.js +37 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.js +182 -0
- package/dist/commands/senders.d.ts +3 -0
- package/dist/commands/senders.js +393 -0
- package/dist/commands/sequences.d.ts +3 -0
- package/dist/commands/sequences.js +2322 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.js +109 -0
- package/dist/commands/signals.d.ts +3 -0
- package/dist/commands/signals.js +289 -0
- package/dist/commands/signup-leads.d.ts +3 -0
- package/dist/commands/signup-leads.js +46 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +50 -0
- package/dist/commands/sourcing.d.ts +3 -0
- package/dist/commands/sourcing.js +27 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/supabase.d.ts +3 -0
- package/dist/commands/supabase.js +128 -0
- package/dist/commands/support.d.ts +3 -0
- package/dist/commands/support.js +651 -0
- package/dist/commands/suppressions.d.ts +3 -0
- package/dist/commands/suppressions.js +577 -0
- package/dist/commands/table-ingestions.d.ts +3 -0
- package/dist/commands/table-ingestions.js +145 -0
- package/dist/commands/table-runs.d.ts +3 -0
- package/dist/commands/table-runs.js +277 -0
- package/dist/commands/tables.d.ts +3 -0
- package/dist/commands/tables.js +3505 -0
- package/dist/commands/tags.d.ts +3 -0
- package/dist/commands/tags.js +147 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +246 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +106 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +27 -0
- package/dist/commands/viewers.d.ts +3 -0
- package/dist/commands/viewers.js +72 -0
- package/dist/commands/voice.d.ts +3 -0
- package/dist/commands/voice.js +351 -0
- package/dist/commands/whatsapp.d.ts +3 -0
- package/dist/commands/whatsapp.js +226 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.js +110 -0
- package/dist/commands/worker.d.ts +3 -0
- package/dist/commands/worker.js +60 -0
- package/dist/commands/workflows.d.ts +3 -0
- package/dist/commands/workflows.js +1752 -0
- package/dist/commands/workspace.d.ts +3 -0
- package/dist/commands/workspace.js +94 -0
- package/dist/compact-list-flags.d.ts +7 -0
- package/dist/compact-list-flags.js +12 -0
- package/dist/crm-support-flags.js +14 -5
- package/dist/customer-operation-commands.js +8 -6
- package/dist/diagnostic-uploader.d.ts +3 -1
- package/dist/diagnostic-uploader.js +29 -8
- package/dist/help.d.ts +5 -0
- package/dist/help.js +22 -0
- package/dist/index.d.ts +98 -1
- package/dist/index.js +331 -30719
- package/dist/local-module-loader.d.ts +4 -0
- package/dist/local-module-loader.js +156 -0
- package/dist/visual-commands.js +9 -5
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
- package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +20 -16
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
- package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
- package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +21 -2
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
- package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
- package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
- package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
- package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
- package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
- package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
- package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { readPositiveInt } from "../cli-values.js";
|
|
2
|
+
import { putAvatarBytesToStorage, splitCommaList } from "../command-support.js";
|
|
3
|
+
import { addContractFlags } from "../contract-flags.js";
|
|
4
|
+
import { requestOxygen } from "../http-client.js";
|
|
5
|
+
import { buildSenderLimitsBody, SENDER_LIMITS_FLAG_OVERLAY } from "../sender-limits-flags.js";
|
|
6
|
+
import { readOption } from "../util.js";
|
|
7
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
8
|
+
import { operationPath } from "@oxygen/shared/contracts/operations";
|
|
9
|
+
import { SENDER_LIMITS_SET_OPERATION } from "@oxygen/shared/contracts/sender-limits";
|
|
10
|
+
import { Command, Option } from "commander";
|
|
11
|
+
export function registerSendersFamily(program, handleAsyncAction) {
|
|
12
|
+
program.addCommand(new Command("senders")
|
|
13
|
+
.description("Manage the org's connected LinkedIn sender accounts for Sequencer: list, connect, sync, get details, disconnect, and tune rate limits. This group is LinkedIn/WhatsApp accounts only; the cross-channel identity (one person's name and photo) that owns inboxes as well is `oxygen senders profiles`.")
|
|
14
|
+
.addCommand(new Command("list")
|
|
15
|
+
.description("List connected LinkedIn sender accounts with health status, rate limits, and today's usage. Disconnected accounts are hidden unless you pass --status disconnected.")
|
|
16
|
+
.option("--status <status>", "Filter by sender status: active, paused, disconnected, restricted, or credentials_required.")
|
|
17
|
+
.option("--no-usage", "Skip today's per-account usage counts for a faster, lighter response.")
|
|
18
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches sender accounts carrying ANY of these tags (see `oxygen tags list`).")
|
|
19
|
+
.option("--json", "Print a JSON envelope.")
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
await handleAsyncAction("senders list", options, () => {
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
// The server counts usage unless told not to, so --no-usage must say so.
|
|
24
|
+
params.set("include_usage", options.usage === false ? "false" : "true");
|
|
25
|
+
const status = readOption(options.status);
|
|
26
|
+
if (status)
|
|
27
|
+
params.set("status", status);
|
|
28
|
+
const tags = splitCommaList(options.tag);
|
|
29
|
+
if (tags.length > 0)
|
|
30
|
+
params.set("tag", tags.join(","));
|
|
31
|
+
const suffix = params.toString();
|
|
32
|
+
return requestOxygen(`/api/cli/senders${suffix ? `?${suffix}` : ""}`);
|
|
33
|
+
});
|
|
34
|
+
}))
|
|
35
|
+
.addCommand(new Command("checkpoints")
|
|
36
|
+
.description("List LinkedIn senders stuck on a security checkpoint (2FA / OTP / in-app validation), each with status, warm-up day, today's usage vs. caps, and the latest error — plus a fleet tally of every sender by status. Read-only; no provider call, no credits. Solve one with `senders checkpoint solve <id> <code>`.")
|
|
37
|
+
.option("--json", "Print a JSON envelope.")
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
40
|
+
}))
|
|
41
|
+
.addCommand(new Command("connect")
|
|
42
|
+
.description("Get a hosted sign-in link to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization. Inspect prices first with `oxygen billing commitments --view connection-prices --json`, which creates no authorization. A covering dollar seat and a credit reservation are alternative billing rails. Read the seat coverage and actual commitment before relying on the next monthly billing date, especially on reconnect. Once the workspace is repriced that reservation is 3,400 credits ($34) monthly; with the credit gate active, a connect the balance cannot cover fails with insufficient_credits. Invites, messages and other LinkedIn actions cost 0 credits on top. `oxygen billing seats --json` shows held seats and next-connection eligibility; paying-seat coverage may remain unknown. Inspect `oxygen billing commitments --json` for an account’s actual reservation, and `oxygen billing estimate --linkedin-accounts <n> --json` prices n accounts against your plan. A workspace linked to another organization's plan is billed at that organization. New accounts require --country (the owner's normal LinkedIn login country) and default to syncing only conversations OXYGEN starts; use --inbox-scope all to opt into the full LinkedIn inbox. Use --cookie-auth and --custom-proxy to expose those inputs in the hosted sign-in wizard; their secrets never pass through OXYGEN. Use --count for bulk onboarding; --count above 1 needs --owner-consent (each owner connects their own account). Links are shareable and valid for 10 minutes.")
|
|
43
|
+
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
44
|
+
.option("--country <code>", "Required for new accounts: ISO 3166-1 alpha-2 code for the account owner's normal LinkedIn login country (for example DE or US); one of the 56 countries OXYGEN has LinkedIn proxies in.")
|
|
45
|
+
.option("--sales-nav", "Request Classic + Sales Navigator access during hosted sign-in.")
|
|
46
|
+
.option("--cookie-auth", "Offer LinkedIn cookie authentication in the hosted sign-in wizard. The account owner enters li_at/li_a there; OXYGEN never receives them.")
|
|
47
|
+
.option("--custom-proxy", "Let the account owner enter a custom proxy in the hosted sign-in wizard. OXYGEN never receives the proxy credentials.")
|
|
48
|
+
.option("--count <n>", "Mint N hosted-auth links in one call for bulk onboarding (1-25, default 1). Every link uses the same --country; use separate calls for different login countries. Ignored when reconnecting.")
|
|
49
|
+
.option("--share-link", "Return a shareable connection link for one account; its owner completes authentication.")
|
|
50
|
+
.option("--inbox-scope <scope>", "LinkedIn inbox privacy for new accounts: oxygen_initiated (default) or all. Reconnect preserves the existing account setting.")
|
|
51
|
+
.addOption(new Option("--acknowledge-risk", "Deprecated: connecting no longer needs it. Still accepted so older scripts keep running.").hideHelp())
|
|
52
|
+
.option("--owner-consent", "Required with --count above 1: each account owner connects their own account.")
|
|
53
|
+
.option("--established", "The account is an established profile: skip the ~2-week warm-up a new account gets by default.")
|
|
54
|
+
.option("--json", "Print a JSON envelope.")
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
await handleAsyncAction("senders connect", options, () => {
|
|
57
|
+
const reconnect = readOption(options.reconnect);
|
|
58
|
+
const country = readOption(options.country)?.toUpperCase();
|
|
59
|
+
const count = readPositiveInt(options.count);
|
|
60
|
+
const inboxScope = readOption(options.inboxScope);
|
|
61
|
+
return requestOxygen("/api/cli/senders/connect", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: {
|
|
64
|
+
...(reconnect ? { reconnect_connection_id: reconnect } : {}),
|
|
65
|
+
...(country ? { country } : {}),
|
|
66
|
+
...(options.salesNav ? { sales_nav: true } : {}),
|
|
67
|
+
...(options.cookieAuth ? { cookie_auth: true } : {}),
|
|
68
|
+
...(options.customProxy ? { custom_proxy: true } : {}),
|
|
69
|
+
...(count ? { count } : {}),
|
|
70
|
+
...(options.shareLink ? { share_link: true } : {}),
|
|
71
|
+
...(inboxScope ? { inbox_sync_scope: inboxScope } : {}),
|
|
72
|
+
...(options.acknowledgeRisk ? { acknowledge_linkedin_risk: true } : {}),
|
|
73
|
+
...(options.ownerConsent ? { owner_consent: true } : {}),
|
|
74
|
+
...(options.established ? { established: true } : {}),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}))
|
|
79
|
+
.addCommand(new Command("inbox-scope")
|
|
80
|
+
.description("Inspect or change which LinkedIn conversations OXYGEN stores for one sender. Scope changes are internal and zero-credit: they do not sync LinkedIn or send a message.")
|
|
81
|
+
.addCommand(new Command("get")
|
|
82
|
+
.description("Show whether a sender syncs only OXYGEN-started conversations or its entire LinkedIn inbox.")
|
|
83
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
84
|
+
.option("--json", "Print a JSON envelope.")
|
|
85
|
+
.action(async (id, options) => {
|
|
86
|
+
await handleAsyncAction("senders inbox-scope get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/inbox-scope`));
|
|
87
|
+
}))
|
|
88
|
+
.addCommand(new Command("set")
|
|
89
|
+
.description("Set LinkedIn inbox privacy. oxygen_initiated hides and stops ingesting unrelated chats; all opts into the full inbox. This internal, zero-credit change does not trigger a provider sync or send a message. Existing stored messages are not deleted.")
|
|
90
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
91
|
+
.requiredOption("--scope <scope>", "oxygen_initiated or all.")
|
|
92
|
+
.option("--json", "Print a JSON envelope.")
|
|
93
|
+
.action(async (id, options) => {
|
|
94
|
+
await handleAsyncAction("senders inbox-scope set", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/inbox-scope`, {
|
|
95
|
+
method: "PATCH",
|
|
96
|
+
body: { scope: options.scope },
|
|
97
|
+
}));
|
|
98
|
+
})))
|
|
99
|
+
.addCommand(new Command("proxy-country")
|
|
100
|
+
.description("Move a LinkedIn sender's automatic proxy to another country without reconnecting. The account picks a proxy there at once when one is free; otherwise its sequences stay paused and each `senders sync` asks again. Zero credits.")
|
|
101
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
102
|
+
.requiredOption("--country <code>", "ISO 3166-1 alpha-2 country where the account owner logs in; one of the 56 countries OXYGEN has LinkedIn proxies in (others are refused with the list).")
|
|
103
|
+
.option("--json", "Print a JSON envelope.")
|
|
104
|
+
.action(async (id, options) => {
|
|
105
|
+
await handleAsyncAction("senders proxy-country", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/proxy-country`, {
|
|
106
|
+
method: "PATCH",
|
|
107
|
+
body: { country: options.country.trim().toUpperCase() },
|
|
108
|
+
}));
|
|
109
|
+
}))
|
|
110
|
+
.addCommand(new Command("get")
|
|
111
|
+
.description("Get one LinkedIn sender account with status, limits, daily-reset timezone, and usage. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
112
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
113
|
+
.option("--json", "Print a JSON envelope.")
|
|
114
|
+
.action(async (id, options) => {
|
|
115
|
+
await handleAsyncAction("senders get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}`));
|
|
116
|
+
}))
|
|
117
|
+
.addCommand(new Command("tag")
|
|
118
|
+
.description("Replace a sender account's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the senders behind one campaign tag. See `oxygen tags list` for the vocabulary.")
|
|
119
|
+
.argument("<id>", "Sender account id or Unipile account id.")
|
|
120
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
121
|
+
.option("--json", "Print a JSON envelope.")
|
|
122
|
+
.action(async (id, options) => {
|
|
123
|
+
await handleAsyncAction("senders tag", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/tags`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
body: { tags: splitCommaList(options.tags) },
|
|
126
|
+
}));
|
|
127
|
+
}))
|
|
128
|
+
.addCommand(new Command("health")
|
|
129
|
+
.description("Show a one-call health snapshot for a LinkedIn sender: status, the latest error reason, any open security checkpoint, today's usage, the warm-up ramp, when the daily quota resets, and inbox sync (last sync, latest failure, back-off). Read-only — no provider call, no credits. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
130
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
131
|
+
.option("--json", "Print a JSON envelope.")
|
|
132
|
+
.action(async (id, options) => {
|
|
133
|
+
await handleAsyncAction("senders health", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/health`));
|
|
134
|
+
}))
|
|
135
|
+
.addCommand(new Command("inmail-balance")
|
|
136
|
+
.description("Show remaining LinkedIn InMail credits for a sender (Premium / Sales Navigator / Recruiter each hold a separate pool). Run before enrolling an `inmail` sequencer step so a campaign does not stall on an account with zero credits. Read-only — not billed. <id> defaults to the org's primary sender.")
|
|
137
|
+
.argument("[id]", "Sender account id, connection id, or Unipile account id. Defaults to the org's primary sender.")
|
|
138
|
+
.option("--service <service>", "Narrow to one credit pool: classic | sales_navigator | recruiter. Defaults to every subscribed service.")
|
|
139
|
+
.option("--json", "Print a JSON envelope.")
|
|
140
|
+
.action(async (id, options) => {
|
|
141
|
+
await handleAsyncAction("senders inmail-balance", options, () => {
|
|
142
|
+
const query = new URLSearchParams();
|
|
143
|
+
if (id)
|
|
144
|
+
query.set("account", id);
|
|
145
|
+
const service = readOption(options.service);
|
|
146
|
+
if (service)
|
|
147
|
+
query.set("service", service);
|
|
148
|
+
const suffix = query.toString();
|
|
149
|
+
return requestOxygen(`/api/cli/linkedin/inmail-balance${suffix ? `?${suffix}` : ""}`);
|
|
150
|
+
});
|
|
151
|
+
}))
|
|
152
|
+
.addCommand(new Command("sync")
|
|
153
|
+
.description("Refresh LinkedIn account state (status, profile) from Unipile. Pass --connection-id to sync one account, or omit it to sync all.")
|
|
154
|
+
.option("--connection-id <id>", "Sync a specific connection id. Defaults to syncing all connected accounts.")
|
|
155
|
+
.option("--json", "Print a JSON envelope.")
|
|
156
|
+
.action(async (options) => {
|
|
157
|
+
await handleAsyncAction("senders sync", options, () => {
|
|
158
|
+
const connectionId = readOption(options.connectionId);
|
|
159
|
+
return requestOxygen("/api/cli/senders/sync", {
|
|
160
|
+
method: "POST",
|
|
161
|
+
body: {
|
|
162
|
+
...(connectionId ? { connection_id: connectionId } : {}),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}))
|
|
167
|
+
.addCommand(new Command("disconnect")
|
|
168
|
+
.description("Disconnect a LinkedIn sender account so it stops sending. Works on an account that is already disconnected. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
169
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
170
|
+
.option("--json", "Print a JSON envelope.")
|
|
171
|
+
.action(async (id, options) => {
|
|
172
|
+
await handleAsyncAction("senders disconnect", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/disconnect`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
}));
|
|
175
|
+
}))
|
|
176
|
+
.addCommand(new Command("checkpoint")
|
|
177
|
+
.description("Resolve a LinkedIn security checkpoint (2FA / OTP / in-app validation) so a stuck sender can return to active. Account administration — no credits, no approval gate.")
|
|
178
|
+
.option("--json", "Print a JSON envelope.")
|
|
179
|
+
.addCommand(new Command("solve")
|
|
180
|
+
.description("Submit the LinkedIn checkpoint code Unipile is waiting on to finish authentication. <id> accepts a sender account id, connection id, or Unipile account id; <code> is the verification code LinkedIn sent.")
|
|
181
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
182
|
+
.argument("<code>", "The verification code LinkedIn sent (2FA / OTP / in-app validation).")
|
|
183
|
+
.option("--json", "Print a JSON envelope.")
|
|
184
|
+
.action(async (id, code, options) => {
|
|
185
|
+
await handleAsyncAction("senders checkpoint solve", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/checkpoint/solve`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: { code },
|
|
188
|
+
}));
|
|
189
|
+
}))
|
|
190
|
+
.addCommand(new Command("resend")
|
|
191
|
+
.description("Ask LinkedIn to resend the checkpoint code so you can read a fresh one, then submit it with `senders checkpoint solve`. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
192
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
193
|
+
.option("--json", "Print a JSON envelope.")
|
|
194
|
+
.action(async (id, options) => {
|
|
195
|
+
await handleAsyncAction("senders checkpoint resend", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/checkpoint/resend`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
}));
|
|
198
|
+
})))
|
|
199
|
+
.addCommand(new Command("limits")
|
|
200
|
+
.description("View and adjust per-account daily action limits, working hours (the times LinkedIn actions may run) and the timezone they use.")
|
|
201
|
+
.option("--json", "Print a JSON envelope.")
|
|
202
|
+
.addCommand(new Command("get")
|
|
203
|
+
.description("Show current limits, overrides, working hours (timezone, days, start, end), defaults, hard maximums, and the caution levels above which LinkedIn restriction risk rises, for an account. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
204
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
205
|
+
.option("--json", "Print a JSON envelope.")
|
|
206
|
+
.action(async (id, options) => {
|
|
207
|
+
await handleAsyncAction("senders limits get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
|
|
208
|
+
}))
|
|
209
|
+
.addCommand(addContractFlags(new Command(SENDER_LIMITS_SET_OPERATION.cli.path.at(-1) ?? "set")
|
|
210
|
+
.description(SENDER_LIMITS_SET_OPERATION.cli.summary)
|
|
211
|
+
.argument("<id>", SENDER_LIMITS_SET_OPERATION.cli.argument?.description), SENDER_LIMITS_SET_OPERATION.contract, SENDER_LIMITS_FLAG_OVERLAY)
|
|
212
|
+
.option("--json", "Print a JSON envelope.")
|
|
213
|
+
.action(async (id, options) => {
|
|
214
|
+
await handleAsyncAction("senders limits set", options, () => {
|
|
215
|
+
const body = buildSenderLimitsBody(options);
|
|
216
|
+
return requestOxygen(operationPath(SENDER_LIMITS_SET_OPERATION, { id }), {
|
|
217
|
+
method: SENDER_LIMITS_SET_OPERATION.method,
|
|
218
|
+
body,
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
})))
|
|
222
|
+
.addCommand(new Command("profiles")
|
|
223
|
+
.description("Sender profiles: group a person's LinkedIn + WhatsApp senders and email inboxes into one sending identity — one name, one first/last name, one photo. A sequence can then send every channel from the same persona, and `oxygen managed-inboxes subscribe/add-inboxes --sender <id>` orders new mailboxes under that identity instead of making you retype it per mailbox. Every email inbox always belongs to one of these profiles, so attaching moves an inbox and detaching one is refused. Manage: list, get, create, update, set-photo, attach/detach accounts, delete.")
|
|
224
|
+
.addCommand(new Command("list")
|
|
225
|
+
.description("List sender profiles with their per-channel account counts (LinkedIn / WhatsApp / inboxes).")
|
|
226
|
+
.option("--status <status>", "Filter by status: active, paused, or archived.")
|
|
227
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches sender profiles carrying ANY of these tags (see `oxygen tags list`).")
|
|
228
|
+
.option("--json", "Print a JSON envelope.")
|
|
229
|
+
.action(async (options) => {
|
|
230
|
+
await handleAsyncAction("senders profiles list", options, () => {
|
|
231
|
+
const params = new URLSearchParams();
|
|
232
|
+
const status = readOption(options.status);
|
|
233
|
+
if (status)
|
|
234
|
+
params.set("status", status);
|
|
235
|
+
const tags = splitCommaList(options.tag);
|
|
236
|
+
if (tags.length > 0)
|
|
237
|
+
params.set("tag", tags.join(","));
|
|
238
|
+
const suffix = params.toString();
|
|
239
|
+
return requestOxygen(`/api/cli/senders/profiles${suffix ? `?${suffix}` : ""}`);
|
|
240
|
+
});
|
|
241
|
+
}))
|
|
242
|
+
.addCommand(new Command("tag")
|
|
243
|
+
.description("Replace a sender profile's workspace tags (whole set; `--tags \"\"` clears) — one edit pools the whole sending identity behind a campaign tag. Member accounts keep their own tags. See `oxygen tags list` for the vocabulary.")
|
|
244
|
+
.argument("<id>", "Sender profile id.")
|
|
245
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
246
|
+
.option("--json", "Print a JSON envelope.")
|
|
247
|
+
.action(async (id, options) => {
|
|
248
|
+
await handleAsyncAction("senders profiles tag", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}/tags`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
body: { tags: splitCommaList(options.tags) },
|
|
251
|
+
}));
|
|
252
|
+
}))
|
|
253
|
+
.addCommand(new Command("get")
|
|
254
|
+
.description("Get one sender profile with the LinkedIn/WhatsApp senders and inboxes attached to it.")
|
|
255
|
+
.argument("<id>", "Sender profile id.")
|
|
256
|
+
.option("--json", "Print a JSON envelope.")
|
|
257
|
+
.action(async (id, options) => {
|
|
258
|
+
await handleAsyncAction("senders profiles get", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}`));
|
|
259
|
+
}))
|
|
260
|
+
.addCommand(new Command("create")
|
|
261
|
+
.description("Create a sender profile: one person's sending identity — their LinkedIn/WhatsApp senders and email inboxes under one name and photo. --from-linkedin seeds the name + avatar from a LinkedIn account and attaches it; then assign inboxes and WhatsApp with --mailboxes / --senders. Without --from-linkedin, --name is required. Give the profile --first-name and --last-name too: those are the exact names the inbox vendor stamps when you order mailboxes with `oxygen managed-inboxes subscribe --sender <id>`, and the vendor has no way to change a name after provisioning. Attaching a LinkedIn sender (--from-linkedin or --senders) gives the profile that account's LinkedIn photo, mirrored into Oxygen storage, unless you pass --avatar-url; a photo you upload later with set-photo --file always wins over the LinkedIn one.")
|
|
262
|
+
.option("--name <name>", "Display name for the profile. Optional when --from-linkedin is given (derived from the LinkedIn account).")
|
|
263
|
+
.option("--first-name <name>", "The person's first name, as it should appear on mailboxes ordered for this sender. Defaults to splitting --name / the LinkedIn name on the first space.")
|
|
264
|
+
.option("--last-name <name>", "The person's last name, as it should appear on mailboxes ordered for this sender. Ordering managed inboxes for this sender needs both names; a one-word name leaves it blank and the order is refused rather than guessed.")
|
|
265
|
+
.option("--from-linkedin <senderId>", "Seed the profile from this LinkedIn/WhatsApp sender account id: derives the name and MIRRORS the LinkedIn photo into Oxygen storage (LinkedIn's own URL expires, so a mirrored copy is what an inbox vendor can still fetch hours later) and attaches the account.")
|
|
266
|
+
.option("--avatar-from-linkedin", "Use the attached LinkedIn account's mirrored photo for this new profile.")
|
|
267
|
+
.option("--avatar-upload-key <key>", "Use the storage_key returned by managed-inboxes upload-avatar; verified server-side.")
|
|
268
|
+
.option("--avatar-url <url>", "Use this image URL as the profile photo instead of the LinkedIn one. Must be a public https URL. An expiring link (e.g. media.licdn.com with e=<epoch>) is stored but marked non-durable and is NOT sent to an inbox vendor — use `set-photo --file` to host a permanent copy.")
|
|
269
|
+
.option("--status <status>", "Initial status: active (default), paused, or archived.")
|
|
270
|
+
.option("--senders <ids>", "Comma-separated sender account ids (LinkedIn/WhatsApp) to attach. A LinkedIn sender also supplies the profile's photo when it has none of its own.")
|
|
271
|
+
.option("--mailboxes <ids>", "Comma-separated email mailbox ids to attach.")
|
|
272
|
+
.option("--json", "Print a JSON envelope.")
|
|
273
|
+
.action(async (options) => {
|
|
274
|
+
await handleAsyncAction("senders profiles create", options, () => requestOxygen("/api/cli/senders/profiles", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
body: {
|
|
277
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
278
|
+
...(readOption(options.firstName) ? { first_name: readOption(options.firstName) } : {}),
|
|
279
|
+
...(readOption(options.lastName) ? { last_name: readOption(options.lastName) } : {}),
|
|
280
|
+
...(readOption(options.fromLinkedin) ? { from_sender_account_id: readOption(options.fromLinkedin) } : {}),
|
|
281
|
+
...(options.avatarFromLinkedin ? { avatar_from_linkedin: true } : {}),
|
|
282
|
+
...(readOption(options.avatarUploadKey) ? { avatar_upload_key: readOption(options.avatarUploadKey) } : {}),
|
|
283
|
+
...(readOption(options.avatarUrl) ? { avatar_url: readOption(options.avatarUrl) } : {}),
|
|
284
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
285
|
+
...(options.senders ? { sender_account_ids: splitCommaList(options.senders) } : {}),
|
|
286
|
+
...(options.mailboxes ? { mailbox_ids: splitCommaList(options.mailboxes) } : {}),
|
|
287
|
+
},
|
|
288
|
+
}));
|
|
289
|
+
}))
|
|
290
|
+
.addCommand(new Command("update")
|
|
291
|
+
.description("Update a sender profile's own fields (display name, first/last name, avatar, status). Use `attach`/`detach` to change its accounts, and `set-photo` for the full photo menu (upload a file, mirror LinkedIn again, or clear).")
|
|
292
|
+
.argument("<id>", "Sender profile id.")
|
|
293
|
+
.option("--name <name>", "New display name.")
|
|
294
|
+
.option("--first-name <name>", "New first name — the exact one an inbox vendor stamps on mailboxes ordered for this sender.")
|
|
295
|
+
.option("--last-name <name>", "New last name — the exact one an inbox vendor stamps on mailboxes ordered for this sender. Fix it BEFORE ordering: the vendor cannot rename a mailbox afterwards.")
|
|
296
|
+
.option("--avatar-url <url>", "New avatar image URL (empty string clears it). Must be a public https URL; an expiring link is stored but marked non-durable and is NOT sent to an inbox vendor.")
|
|
297
|
+
.option("--status <status>", "New status: active, paused, or archived.")
|
|
298
|
+
.option("--json", "Print a JSON envelope.")
|
|
299
|
+
.action(async (id, options) => {
|
|
300
|
+
await handleAsyncAction("senders profiles update", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}`, {
|
|
301
|
+
method: "PATCH",
|
|
302
|
+
body: {
|
|
303
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
304
|
+
...(readOption(options.firstName) ? { first_name: readOption(options.firstName) } : {}),
|
|
305
|
+
...(readOption(options.lastName) ? { last_name: readOption(options.lastName) } : {}),
|
|
306
|
+
...(options.avatarUrl !== undefined ? { avatar_url: readOption(options.avatarUrl) ?? "" } : {}),
|
|
307
|
+
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
308
|
+
},
|
|
309
|
+
}));
|
|
310
|
+
}))
|
|
311
|
+
.addCommand(new Command("set-photo")
|
|
312
|
+
.description("Set the reusable sender identity's profile picture for FUTURE managed-inbox orders made with --sender <id>. Pick exactly one source: --file (upload your own image; Oxygen hosts it permanently), --url (a public https image), --from-linkedin (re-mirror the photo from the attached LinkedIn account), or --clear. Hosting matters: an inbox vendor fetches the photo HOURS after an order, so an expiring LinkedIn CDN URL can ship the mailbox faceless. This updates Oxygen's sender profile only: it does not change already-provisioned mailbox photos or call the inbox provider. For a one-off typed-name order using profile_picture_url in --mailboxes JSON, use `oxygen managed-inboxes upload-avatar <path>` instead. Precedence: a photo you upload (--file) or set with --url is yours and is never overwritten by Oxygen; a sender with a LinkedIn account attached and no photo of its own gets that LinkedIn photo automatically (existing senders are backfilled), so --clear on such a sender is temporary — upload your own photo to override LinkedIn's. If LinkedIn's CDN refuses the stored picture, Oxygen re-syncs the account and retries once; read back avatar_durable, where false means only an expiring link could be kept. Free — no credits.")
|
|
313
|
+
.argument("<id>", "Sender profile id (from `oxygen senders profiles list`).")
|
|
314
|
+
.option("--file <path>", "Upload a PNG, JPEG, or WebP from disk (max 8MB) and use it. Oxygen hosts the image permanently, so an inbox vendor can still fetch it at provisioning time.")
|
|
315
|
+
.option("--url <url>", "Use an image already published at a public https URL. If the link expires (e.g. media.licdn.com with e=<epoch>) the photo is kept for Oxygen's own UI but marked non-durable and NEVER sent to an inbox vendor.")
|
|
316
|
+
.option("--from-linkedin", "Re-mirror the photo from this profile's attached LinkedIn account into Oxygen storage. Use it when the person changed their LinkedIn picture. If the CDN refuses the stored picture, the account is re-synced and the mirror retried once; when it still fails, the expiring link is kept, avatar_durable reads false, and the worker retries later.")
|
|
317
|
+
.option("--clear", "Remove the photo. Mailboxes ordered afterwards ship without one. On a sender with a LinkedIn account attached this is temporary — the LinkedIn photo is adopted again on the next sync; upload your own photo to override it instead.")
|
|
318
|
+
.option("--json", "Print a JSON envelope.")
|
|
319
|
+
.action(async (id, options) => {
|
|
320
|
+
await handleAsyncAction("senders profiles set-photo", options, async () => {
|
|
321
|
+
const filePath = readOption(options.file);
|
|
322
|
+
const url = readOption(options.url);
|
|
323
|
+
const chosen = [
|
|
324
|
+
filePath ? "--file" : null,
|
|
325
|
+
url ? "--url" : null,
|
|
326
|
+
options.fromLinkedin ? "--from-linkedin" : null,
|
|
327
|
+
options.clear ? "--clear" : null,
|
|
328
|
+
].filter((flag) => flag !== null);
|
|
329
|
+
// Refuse locally rather than letting the server pick a winner: the
|
|
330
|
+
// photo is what an inbox vendor stamps permanently, so "which source
|
|
331
|
+
// won" is never a question a caller should have to reverse-engineer
|
|
332
|
+
// from the response.
|
|
333
|
+
if (chosen.length === 0) {
|
|
334
|
+
throw new OxygenError("invalid_request", "Pass one photo source: --file <path>, --url <https url>, --from-linkedin, or --clear.", { exitCode: 2 });
|
|
335
|
+
}
|
|
336
|
+
if (chosen.length > 1) {
|
|
337
|
+
throw new OxygenError("conflicting_flags", `Pass exactly one photo source, not ${chosen.join(" + ")}.`, { exitCode: 2 });
|
|
338
|
+
}
|
|
339
|
+
// --file stages the bytes first, then finishes on the profile write
|
|
340
|
+
// like every other source: one response, carrying the profile and its
|
|
341
|
+
// deep-link, instead of leaving the caller holding a storage key and
|
|
342
|
+
// an unfinished profile.
|
|
343
|
+
const body = filePath
|
|
344
|
+
? { avatar_upload_key: await putAvatarBytesToStorage(filePath) }
|
|
345
|
+
: url
|
|
346
|
+
? { avatar_url: url }
|
|
347
|
+
: options.fromLinkedin
|
|
348
|
+
? { avatar_from_linkedin: true }
|
|
349
|
+
: { avatar_url: "" };
|
|
350
|
+
return await requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}`, {
|
|
351
|
+
method: "PATCH",
|
|
352
|
+
body,
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}))
|
|
356
|
+
.addCommand(new Command("attach")
|
|
357
|
+
.description("Attach senders and/or inboxes to a profile. An account belongs to one profile at a time — attaching moves it. Attaching a LinkedIn sender to a profile without a photo of its own gives it that account's LinkedIn photo (mirrored into Oxygen storage); an uploaded or URL-set photo is never replaced. Read back has_photo and avatar_durable in the result.")
|
|
358
|
+
.argument("<id>", "Sender profile id.")
|
|
359
|
+
.option("--senders <ids>", "Comma-separated sender account ids (LinkedIn/WhatsApp) to attach. A LinkedIn sender also supplies the profile's photo when it has none of its own.")
|
|
360
|
+
.option("--mailboxes <ids>", "Comma-separated email mailbox ids to attach.")
|
|
361
|
+
.option("--json", "Print a JSON envelope.")
|
|
362
|
+
.action(async (id, options) => {
|
|
363
|
+
await handleAsyncAction("senders profiles attach", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}`, {
|
|
364
|
+
method: "PATCH",
|
|
365
|
+
body: {
|
|
366
|
+
...(options.senders ? { sender_account_ids: splitCommaList(options.senders) } : {}),
|
|
367
|
+
...(options.mailboxes ? { mailbox_ids: splitCommaList(options.mailboxes) } : {}),
|
|
368
|
+
},
|
|
369
|
+
}));
|
|
370
|
+
}))
|
|
371
|
+
.addCommand(new Command("detach")
|
|
372
|
+
.description("Detach LinkedIn/WhatsApp senders from a profile, returning them to the unassigned pool. --mailboxes is refused: an email inbox always belongs to a sender, so there is no unassigned pool for it — move it with `senders profiles attach <other-profile> --mailboxes <id>`, which reassigns it.")
|
|
373
|
+
.argument("<id>", "Sender profile id.")
|
|
374
|
+
.option("--senders <ids>", "Comma-separated sender account ids to detach.")
|
|
375
|
+
.option("--mailboxes <ids>", "Comma-separated email mailbox ids to detach.")
|
|
376
|
+
.option("--json", "Print a JSON envelope.")
|
|
377
|
+
.action(async (id, options) => {
|
|
378
|
+
await handleAsyncAction("senders profiles detach", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}`, {
|
|
379
|
+
method: "PATCH",
|
|
380
|
+
body: {
|
|
381
|
+
...(options.senders ? { detach_sender_account_ids: splitCommaList(options.senders) } : {}),
|
|
382
|
+
...(options.mailboxes ? { detach_mailbox_ids: splitCommaList(options.mailboxes) } : {}),
|
|
383
|
+
},
|
|
384
|
+
}));
|
|
385
|
+
}))
|
|
386
|
+
.addCommand(new Command("delete")
|
|
387
|
+
.description("Delete a sender profile. Refused while the profile still owns inboxes — move them to another profile first; LinkedIn/WhatsApp accounts detach (return to the pool) and are never deleted.")
|
|
388
|
+
.argument("<id>", "Sender profile id.")
|
|
389
|
+
.option("--json", "Print a JSON envelope.")
|
|
390
|
+
.action(async (id, options) => {
|
|
391
|
+
await handleAsyncAction("senders profiles delete", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}`, { method: "DELETE" }));
|
|
392
|
+
}))));
|
|
393
|
+
}
|