@oxygen-agent/cli 1.1014.136 → 1.1014.618
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 +1669 -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 +3492 -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 +17 -14
- 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.js +5 -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,577 @@
|
|
|
1
|
+
import { emitCliFailure, writeCreditsReceipt, writeJson } from "../cli-output.js";
|
|
2
|
+
import { parseJsonObject, readPositiveInt } from "../cli-values.js";
|
|
3
|
+
import { parseJsonValue, readFileIfPresent, readJsonFileValue, readPositiveNumber, splitCommaList } from "../command-support.js";
|
|
4
|
+
import { requestOxygen } from "../http-client.js";
|
|
5
|
+
import { readOption } from "../util.js";
|
|
6
|
+
import { addSuppressionListFlags, suppressionListQuery } from "../workspace-utility-flags.js";
|
|
7
|
+
import { OxygenError, success } from "@oxygen/shared/cli-result";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
function readSuppressionBulkKeys(value, flag) {
|
|
12
|
+
const entries = splitCommaList(value);
|
|
13
|
+
if (entries.length === 0 || entries.length > 500) {
|
|
14
|
+
throw new OxygenError("invalid_request", `${flag} requires 1-500 nonblank entries.`, { exitCode: 2 });
|
|
15
|
+
}
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
18
|
+
function suppressionListParams(options) {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
const reason = readOption(options.reason);
|
|
21
|
+
const search = readOption(options.search);
|
|
22
|
+
const limit = readOption(options.limit);
|
|
23
|
+
const offset = readOption(options.offset);
|
|
24
|
+
if (reason)
|
|
25
|
+
params.set("reason", reason);
|
|
26
|
+
if (search)
|
|
27
|
+
params.set("search", search);
|
|
28
|
+
if (limit)
|
|
29
|
+
params.set("limit", limit);
|
|
30
|
+
if (offset)
|
|
31
|
+
params.set("offset", offset);
|
|
32
|
+
return params;
|
|
33
|
+
}
|
|
34
|
+
export function registerSuppressionsFamily(program, handleAsyncAction) {
|
|
35
|
+
program.addCommand(new Command("suppressions")
|
|
36
|
+
.description("Unified do-not-contact controls for people and typed identities. `policy` decides WHO GETS ADDED automatically -- which replies, bounces, unsubscribes and call outcomes put someone on the list, and whether each one blocks them forever or only stops the current campaign. `crm-backfill` files the people already on the list into your CRM. `import-person` bundles one contact's email, LinkedIn, phone, and explicit company domain; `import` preserves the legacy mixed-file contract; `import-identities` is the integration-level typed endpoint; `hubspot lists|sync` arms additive synchronization. Company DNC applies across Sequence channels and is never inferred from a person's email. Consumes 0 credits.")
|
|
37
|
+
.addCommand(new Command("policy")
|
|
38
|
+
.description("Do-not-contact RULES: who gets added to the list automatically. Seven events can add someone -- an email hard bounce, a one-click unsubscribe, opt-out wording in a reply, ANY inbound LinkedIn reply, ANY inbound WhatsApp reply, and the do-not-call / wrong-number call outcomes -- plus an optional rule per inbox reply classification. Each rule is off, stop_only (end this campaign but do not block future ones) or suppress (the default for all seven, and what every version before this did). Blocking itself is never configurable: once someone is on the list they are always skipped. Consumes 0 credits.")
|
|
39
|
+
.addCommand(new Command("show")
|
|
40
|
+
.description("Show every do-not-contact rule with its current setting, whether that is your change or the shipped default, and the values it accepts.")
|
|
41
|
+
.option("--json", "Print a JSON envelope.")
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
await handleAsyncAction("suppressions policy", options, () => requestOxygen("/api/cli/suppressions/policy"));
|
|
44
|
+
}))
|
|
45
|
+
.addCommand(new Command("set")
|
|
46
|
+
.description("Change one rule. Example: `oxygen suppressions policy set --rule linkedin_reply --effect stop_only` stops a LinkedIn replier's current campaign without blocking them from every future one.")
|
|
47
|
+
.requiredOption("--rule <key>", "Rule key from `policy show` (e.g. linkedin_reply, whatsapp_reply, email_hard_bounce, reply_status:not_interested).")
|
|
48
|
+
.requiredOption("--effect <effect>", "off (do nothing), stop_only (end this enrollment only) or suppress (also add to the do-not-contact list).")
|
|
49
|
+
.option("--reason <reason>", "Reason stamped on the list entry when the rule suppresses. Defaults to the rule's own.")
|
|
50
|
+
.option("--json", "Print a JSON envelope.")
|
|
51
|
+
.action(async (options) => {
|
|
52
|
+
await handleAsyncAction("suppressions policy set", options, () => {
|
|
53
|
+
const rule = readOption(options.rule);
|
|
54
|
+
if (!rule)
|
|
55
|
+
throw new Error("--rule is required.");
|
|
56
|
+
const effect = readOption(options.effect);
|
|
57
|
+
if (!effect)
|
|
58
|
+
throw new Error("--effect is required.");
|
|
59
|
+
const reason = readOption(options.reason);
|
|
60
|
+
return requestOxygen("/api/cli/suppressions/policy", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: { rule, effect, ...(reason ? { reason } : {}) },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}))
|
|
66
|
+
.addCommand(new Command("reset")
|
|
67
|
+
.description("Return one rule (--rule) or every rule (--all) to the shipped default.")
|
|
68
|
+
.option("--rule <key>", "Rule key to reset.")
|
|
69
|
+
.option("--all", "Reset every rule instead of one.")
|
|
70
|
+
.option("--json", "Print a JSON envelope.")
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
await handleAsyncAction("suppressions policy set", options, () => {
|
|
73
|
+
const rule = readOption(options.rule);
|
|
74
|
+
if (options.all && rule)
|
|
75
|
+
throw new Error("Pass either --rule or --all, not both.");
|
|
76
|
+
if (!options.all && !rule)
|
|
77
|
+
throw new Error("--rule is required (or pass --all).");
|
|
78
|
+
return requestOxygen("/api/cli/suppressions/policy", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: options.all ? { reset_all: true } : { rule, reset: true },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
})))
|
|
84
|
+
.addCommand(new Command("crm-backfill")
|
|
85
|
+
.description("Create a CRM person record for everyone already on the do-not-contact list, so you can see who they are. New suppressions do this automatically; this covers the ones added before. Previews by default and writes nothing until you pass --live. Consumes 0 credits: it never runs enrichment on the records it creates.")
|
|
86
|
+
.option("--live", "Actually create the records. Without this the command only reports what it would create.")
|
|
87
|
+
.option("--limit <n>", "People to process in this call (1-500; default 200).")
|
|
88
|
+
.option("--offset <n>", "Resume cursor; pass the next_offset from the previous call.")
|
|
89
|
+
.option("--json", "Print a JSON envelope.")
|
|
90
|
+
.action(async (options) => {
|
|
91
|
+
await handleAsyncAction("suppressions crm-backfill", options, () => {
|
|
92
|
+
const limit = readOption(options.limit);
|
|
93
|
+
const offset = readOption(options.offset);
|
|
94
|
+
return requestOxygen("/api/cli/suppressions/crm-backfill", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: {
|
|
97
|
+
...(options.live ? { live: true } : {}),
|
|
98
|
+
...(limit ? { limit: Number(limit) } : {}),
|
|
99
|
+
...(offset ? { offset: Number(offset) } : {}),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}))
|
|
104
|
+
.addCommand(new Command("list")
|
|
105
|
+
.description("List the org's do-not-contact suppressions, newest first. Filter by --reason or by --search (case-insensitive substring of the lead provider id).")
|
|
106
|
+
.option("--reason <reason>", "Filter by reason: manual, replied, unsubscribed, bounced, do_not_contact, friends.")
|
|
107
|
+
.option("--search <text>", "Case-insensitive substring match on the lead provider id.")
|
|
108
|
+
.option("--limit <n>", "Maximum suppressions to return (1-500; default 100).")
|
|
109
|
+
.option("--offset <n>", "Pagination offset (0-based).")
|
|
110
|
+
.option("--json", "Print a JSON envelope.")
|
|
111
|
+
.action(async (options) => {
|
|
112
|
+
await handleAsyncAction("suppressions list", options, () => {
|
|
113
|
+
const params = new URLSearchParams();
|
|
114
|
+
const reason = readOption(options.reason);
|
|
115
|
+
if (reason)
|
|
116
|
+
params.set("reason", reason);
|
|
117
|
+
const search = readOption(options.search);
|
|
118
|
+
if (search)
|
|
119
|
+
params.set("search", search);
|
|
120
|
+
const limit = readOption(options.limit);
|
|
121
|
+
if (limit)
|
|
122
|
+
params.set("limit", limit);
|
|
123
|
+
const offset = readOption(options.offset);
|
|
124
|
+
if (offset)
|
|
125
|
+
params.set("offset", offset);
|
|
126
|
+
const suffix = params.toString();
|
|
127
|
+
return requestOxygen(`/api/cli/suppressions${suffix ? `?${suffix}` : ""}`);
|
|
128
|
+
});
|
|
129
|
+
}))
|
|
130
|
+
.addCommand(new Command("add")
|
|
131
|
+
.description("Add or refresh selected lead ids on the org do-not-contact list so the sequencer never enrolls them again. Use --lead or --leads (1-500). Idempotent; --reason defaults to manual.")
|
|
132
|
+
.option("--lead <provider_id>", "One lead provider id to suppress; exclusive with --leads.")
|
|
133
|
+
.option("--leads <ids>", "Comma-separated lead provider ids to suppress in one bounded batch (1-500).")
|
|
134
|
+
.option("--reason <reason>", "Why: manual (default), replied, unsubscribed, bounced, do_not_contact, friends.")
|
|
135
|
+
.option("--detail <text>", "Optional free-text note stored with the suppression.")
|
|
136
|
+
.option("--json", "Print a JSON envelope.")
|
|
137
|
+
.action(async (options) => {
|
|
138
|
+
await handleAsyncAction("suppressions add", options, () => {
|
|
139
|
+
const lead = readOption(options.lead);
|
|
140
|
+
const leads = options.leads !== undefined ? readSuppressionBulkKeys(options.leads, "--leads") : undefined;
|
|
141
|
+
if (Boolean(lead) === Boolean(leads))
|
|
142
|
+
throw new OxygenError("invalid_request", "Pass exactly one of --lead or --leads.", { exitCode: 2 });
|
|
143
|
+
const reason = readOption(options.reason);
|
|
144
|
+
const detail = readOption(options.detail);
|
|
145
|
+
return requestOxygen("/api/cli/suppressions", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: {
|
|
148
|
+
...(leads ? { lead_provider_ids: leads } : { lead_provider_id: lead }),
|
|
149
|
+
...(reason ? { reason } : {}),
|
|
150
|
+
...(detail ? { detail } : {}),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}))
|
|
155
|
+
.addCommand(new Command("remove")
|
|
156
|
+
.description("Remove selected leads or every identity of selected merged people from the org do-not-contact list. Choose one of --lead, --leads, --subject-id or --subject-ids; batches hold 1-500 ids. Read a person's metadata.dnc_subject_id in suppressions list, addresses, phones or companies --json.")
|
|
157
|
+
.option("--lead <provider_id>", "The lead provider id to un-suppress. Mutually exclusive with --subject-id.")
|
|
158
|
+
.option("--subject-id <id>", "DNC subject id (metadata.dnc_subject_id). Clears that person's email, phone, LinkedIn, and company suppressions in one transaction.")
|
|
159
|
+
.option("--leads <ids>", "Comma-separated lead provider ids to remove in one bounded batch (1-500).")
|
|
160
|
+
.option("--subject-ids <ids>", "Comma-separated merged-person subject ids to remove across every ledger (1-500).")
|
|
161
|
+
.option("--json", "Print a JSON envelope.")
|
|
162
|
+
.action(async (options) => {
|
|
163
|
+
if (options.leads !== undefined || options.subjectIds !== undefined) {
|
|
164
|
+
await handleAsyncAction("suppressions remove", options, () => {
|
|
165
|
+
const selectors = [options.lead, options.subjectId, options.leads, options.subjectIds].filter((value) => value !== undefined);
|
|
166
|
+
if (selectors.length !== 1)
|
|
167
|
+
throw new OxygenError("invalid_request", "Pass exactly one of --lead, --leads, --subject-id or --subject-ids.", { exitCode: 2 });
|
|
168
|
+
return requestOxygen("/api/cli/suppressions", {
|
|
169
|
+
method: "DELETE",
|
|
170
|
+
body: options.leads !== undefined
|
|
171
|
+
? { lead_provider_ids: readSuppressionBulkKeys(options.leads, "--leads") }
|
|
172
|
+
: { subject_ids: readSuppressionBulkKeys(options.subjectIds, "--subject-ids") },
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const subjectId = readOption(options.subjectId);
|
|
178
|
+
if (!subjectId) {
|
|
179
|
+
await handleAsyncAction("suppressions remove", options, () => {
|
|
180
|
+
const lead = readOption(options.lead);
|
|
181
|
+
if (!lead)
|
|
182
|
+
throw new Error("Pass --lead <provider_id> or --subject-id <id>.");
|
|
183
|
+
return requestOxygen(`/api/cli/suppressions?lead_provider_id=${encodeURIComponent(lead)}`, {
|
|
184
|
+
method: "DELETE",
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
if (readOption(options.lead)) {
|
|
191
|
+
throw new Error("Pass either --lead or --subject-id, not both.");
|
|
192
|
+
}
|
|
193
|
+
const data = await requestOxygen(`/api/cli/suppressions?subject_id=${encodeURIComponent(subjectId)}`, { method: "DELETE" });
|
|
194
|
+
if (options.json) {
|
|
195
|
+
writeJson(success("suppressions remove", data));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Same compact multi-ledger receipt style as `suppressions import`:
|
|
199
|
+
// the per-channel counts plus up to 10 cleared identifiers (the full
|
|
200
|
+
// list is always in the --json envelope).
|
|
201
|
+
const parsed = (data ?? {});
|
|
202
|
+
const identities = Array.isArray(parsed.removed_identities) ? parsed.removed_identities : [];
|
|
203
|
+
const removedCount = parsed.removed_count ?? 0;
|
|
204
|
+
writeJson({
|
|
205
|
+
subject_id: parsed.subject_id ?? subjectId,
|
|
206
|
+
removed_count: removedCount,
|
|
207
|
+
removed_emails: parsed.removed?.email ?? 0,
|
|
208
|
+
removed_phones: parsed.removed?.phone ?? 0,
|
|
209
|
+
removed_linkedin: parsed.removed?.linkedin ?? 0,
|
|
210
|
+
removed_companies: parsed.removed?.company ?? 0,
|
|
211
|
+
removed_identities: identities.slice(0, 10),
|
|
212
|
+
// Counted off removed_count, not the echoed array: the API caps
|
|
213
|
+
// that list too, so --json is NOT a way to see the rest. The
|
|
214
|
+
// counts above are the exact record of what was cleared.
|
|
215
|
+
...(removedCount > Math.min(identities.length, 10)
|
|
216
|
+
? {
|
|
217
|
+
removed_identities_note: `Showing ${Math.min(identities.length, 10)} of ${removedCount} cleared identifiers; the counts above are exact.`,
|
|
218
|
+
}
|
|
219
|
+
: {}),
|
|
220
|
+
...(parsed.note ? { note: parsed.note } : {}),
|
|
221
|
+
deep_link: parsed.deepLink,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
writeCreditsReceipt(data);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
emitCliFailure("suppressions remove", error);
|
|
228
|
+
}
|
|
229
|
+
}))
|
|
230
|
+
.addCommand(new Command("import")
|
|
231
|
+
.description("Bulk-import a do-not-contact blocklist from a file (newline / comma / whitespace separated, max 5000). An entry with '@' goes on the per-address email list; a bare domain (e.g. acme.com) blocks email to that whole domain; a LinkedIn profile URL or member id (ACo...) lands on the people do-not-contact list; any other URL is rejected. A domain block is a sharp tool, so --reason is REQUIRED when the file contains any domains; otherwise manual is the default. Idempotent. Consumes 0 credits.")
|
|
232
|
+
.requiredOption("--file <path>", "Path to a file of emails, bare domains, LinkedIn profile URLs, and/or LinkedIn member ids, separated by newlines, commas, or whitespace.")
|
|
233
|
+
.option("--reason <reason>", "Suppression reason. Emails: hard_bounce | complaint | unsubscribe | manual. Domains: manual | complaint | policy. LinkedIn contacts: manual | replied | unsubscribed | bounced | do_not_contact | friends. Required when the file contains any domains; must be valid for every entry type present (manual always is).")
|
|
234
|
+
.option("--source <text>", "Optional provenance note stored on every imported row (e.g. instantly_export).")
|
|
235
|
+
.option("--metadata-json <json-or-file>", "Default metadata stored on imported identities.")
|
|
236
|
+
.option("--detail <text>", "Optional free-text note stored on every imported row.")
|
|
237
|
+
.option("--json", "Print a JSON envelope.")
|
|
238
|
+
.action(async (options) => {
|
|
239
|
+
try {
|
|
240
|
+
const file = readOption(options.file);
|
|
241
|
+
if (!file)
|
|
242
|
+
throw new Error("--file is required.");
|
|
243
|
+
const text = readFileSync(resolve(file), "utf8");
|
|
244
|
+
const entries = [
|
|
245
|
+
...new Set(text
|
|
246
|
+
.split(/[\s,]+/)
|
|
247
|
+
.map((entry) => entry.trim())
|
|
248
|
+
.filter((entry) => entry.length > 0)),
|
|
249
|
+
];
|
|
250
|
+
if (entries.length === 0)
|
|
251
|
+
throw new Error(`No emails, domains, or LinkedIn ids found in ${file}.`);
|
|
252
|
+
const reason = readOption(options.reason);
|
|
253
|
+
const source = readOption(options.source);
|
|
254
|
+
const detail = readOption(options.detail);
|
|
255
|
+
const data = await requestOxygen("/api/cli/suppressions/import", {
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: {
|
|
258
|
+
entries,
|
|
259
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
260
|
+
...(reason ? { reason } : {}),
|
|
261
|
+
...(source ? { source } : {}),
|
|
262
|
+
...(detail ? { detail } : {}),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
if (options.json) {
|
|
266
|
+
writeJson(success("suppressions import", data));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Compact human view: the counts plus up to 10 rejected entries (the
|
|
270
|
+
// full list is always in the --json envelope).
|
|
271
|
+
const parsed = (data ?? {});
|
|
272
|
+
const rejected = Array.isArray(parsed.rejected) ? parsed.rejected : [];
|
|
273
|
+
writeJson({
|
|
274
|
+
imported_emails: parsed.imported_emails ?? 0,
|
|
275
|
+
imported_domains: parsed.imported_domains ?? 0,
|
|
276
|
+
imported_contacts: parsed.imported_contacts ?? 0,
|
|
277
|
+
imported_phones: parsed.imported_phones ?? 0,
|
|
278
|
+
imported_company_domains: parsed.imported_company_domains ?? 0,
|
|
279
|
+
imported_linkedin_companies: parsed.imported_linkedin_companies ?? 0,
|
|
280
|
+
accepted_identities: parsed.accepted_identities ?? 0,
|
|
281
|
+
rejected_count: rejected.length,
|
|
282
|
+
rejected: rejected.slice(0, 10),
|
|
283
|
+
...(rejected.length > 10
|
|
284
|
+
? { rejected_note: `+${rejected.length - 10} more (use --json for the full list)` }
|
|
285
|
+
: {}),
|
|
286
|
+
deep_link: parsed.deep_link,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
writeCreditsReceipt(data);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
emitCliFailure("suppressions import", error);
|
|
293
|
+
}
|
|
294
|
+
}))
|
|
295
|
+
.addCommand(new Command("import-identities")
|
|
296
|
+
.description("Import a source-neutral typed JSON array into DNC. Kinds: email, phone, linkedin_person, company_domain, linkedin_company. Preferred for integrations; company identities apply across every Sequence channel and are never inferred from a person's email. A company entry is a permanent block on every campaign; to pause a company and resume it later, use `sequences pause-company`.")
|
|
297
|
+
.requiredOption("--file <path>", "JSON file containing [{kind,value,reason?,source?,detail?,metadata?}], max 5000.")
|
|
298
|
+
.option("--reason <reason>", "Default reason for entries that omit one.")
|
|
299
|
+
.option("--source <text>", "Default provenance for entries that omit one.")
|
|
300
|
+
.option("--detail <text>", "Default note for entries that omit one.")
|
|
301
|
+
.option("--json", "Print a JSON envelope.")
|
|
302
|
+
.action(async (options) => {
|
|
303
|
+
await handleAsyncAction("suppressions import-identities", options, () => {
|
|
304
|
+
const file = readOption(options.file);
|
|
305
|
+
if (!file)
|
|
306
|
+
throw new Error("--file is required.");
|
|
307
|
+
const identities = readJsonFileValue(resolve(file), "--file");
|
|
308
|
+
if (!Array.isArray(identities) || identities.length === 0 || identities.length > 5000) {
|
|
309
|
+
throw new Error("--file must contain a non-empty JSON array with at most 5000 identities.");
|
|
310
|
+
}
|
|
311
|
+
const reason = readOption(options.reason);
|
|
312
|
+
const source = readOption(options.source);
|
|
313
|
+
const detail = readOption(options.detail);
|
|
314
|
+
return requestOxygen("/api/cli/suppressions/import", {
|
|
315
|
+
method: "POST",
|
|
316
|
+
body: {
|
|
317
|
+
identities,
|
|
318
|
+
...(reason ? { reason } : {}),
|
|
319
|
+
...(source ? { source } : {}),
|
|
320
|
+
...(detail ? { detail } : {}),
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}))
|
|
325
|
+
.addCommand(new Command("import-person")
|
|
326
|
+
// Deliberately NOT extended with the subjects[] pointer: this help text
|
|
327
|
+
// is already 199 chars, and the generated CLI reference cell truncates a
|
|
328
|
+
// description past 200 to its FIRST sentence — appending anything drops
|
|
329
|
+
// the "domain is company-wide, never inferred from --email" warning from
|
|
330
|
+
// the public table. The receipt itself carries `subjects[]`, and
|
|
331
|
+
// `suppressions remove --help` names where subject ids come from.
|
|
332
|
+
.description("Import one person as one DNC subject with any combination of email, LinkedIn, E.164 phone, and an explicitly supplied company domain. The domain is company-wide and is never inferred from --email.")
|
|
333
|
+
.option("--name <name>", "Optional contact display name.")
|
|
334
|
+
.option("--email <address>", "Email address to suppress.")
|
|
335
|
+
.option("--linkedin <url_or_id>", "Public LinkedIn person URL or member id to suppress.")
|
|
336
|
+
.option("--phone <e164>", "Strict E.164 phone number to suppress.")
|
|
337
|
+
.option("--domain <domain>", "Explicit company-wide domain block. This affects every Sequence channel.")
|
|
338
|
+
.option("--reason <reason>", "Suppression reason; manual by default and valid for every bundled identifier.")
|
|
339
|
+
.option("--source <text>", "Optional provenance stored on every identifier.")
|
|
340
|
+
.option("--detail <text>", "Optional note stored on every identifier.")
|
|
341
|
+
.option("--json", "Print a JSON envelope.")
|
|
342
|
+
.action(async (options) => {
|
|
343
|
+
await handleAsyncAction("suppressions import-person", options, () => {
|
|
344
|
+
const name = readOption(options.name);
|
|
345
|
+
const email = readOption(options.email);
|
|
346
|
+
const linkedin = readOption(options.linkedin);
|
|
347
|
+
const phone = readOption(options.phone);
|
|
348
|
+
const domain = readOption(options.domain);
|
|
349
|
+
if (!email && !linkedin && !phone && !domain) {
|
|
350
|
+
throw new Error("Provide at least one of --email, --linkedin, --phone, or --domain.");
|
|
351
|
+
}
|
|
352
|
+
const reason = readOption(options.reason);
|
|
353
|
+
const source = readOption(options.source);
|
|
354
|
+
const detail = readOption(options.detail);
|
|
355
|
+
return requestOxygen("/api/cli/suppressions/import", {
|
|
356
|
+
method: "POST",
|
|
357
|
+
body: {
|
|
358
|
+
people: [{
|
|
359
|
+
...(name ? { name } : {}),
|
|
360
|
+
...(email ? { email } : {}),
|
|
361
|
+
...(linkedin ? { linkedin_url: linkedin } : {}),
|
|
362
|
+
...(phone ? { phone } : {}),
|
|
363
|
+
...(domain ? { company_domain: domain } : {}),
|
|
364
|
+
...(reason ? { reason } : {}),
|
|
365
|
+
...(source ? { source } : {}),
|
|
366
|
+
...(detail ? { detail } : {}),
|
|
367
|
+
}],
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}))
|
|
372
|
+
.addCommand(new Command("phones")
|
|
373
|
+
.description("List phone suppressions (strict E.164) used by call, WhatsApp, and Sequence safety gates.")
|
|
374
|
+
.option("--reason <reason>", "do_not_call, wrong_number, complaint, or manual.")
|
|
375
|
+
.option("--search <text>", "Phone substring.")
|
|
376
|
+
.option("--limit <n>", "Maximum rows (1-500).")
|
|
377
|
+
.option("--offset <n>", "Pagination offset.")
|
|
378
|
+
.option("--json", "Print a JSON envelope.")
|
|
379
|
+
.action(async (options) => {
|
|
380
|
+
await handleAsyncAction("suppressions phones", options, () => {
|
|
381
|
+
const params = suppressionListParams(options);
|
|
382
|
+
const suffix = params.toString();
|
|
383
|
+
return requestOxygen(`/api/cli/suppressions/phones${suffix ? `?${suffix}` : ""}`);
|
|
384
|
+
});
|
|
385
|
+
}))
|
|
386
|
+
.addCommand(new Command("remove-phone")
|
|
387
|
+
.description("Remove one phone suppression or 1-500 exact E.164 numbers with --phones.")
|
|
388
|
+
.argument("[phone]", "One strict E.164 number, e.g. +14155550123; exclusive with --phones.")
|
|
389
|
+
.option("--phones <phones>", "Comma-separated strict E.164 numbers to remove (1-500).")
|
|
390
|
+
.option("--json", "Print a JSON envelope.")
|
|
391
|
+
.action(async (phone, options) => {
|
|
392
|
+
await handleAsyncAction("suppressions remove-phone", options, () => {
|
|
393
|
+
if (Boolean(phone) === (options.phones !== undefined))
|
|
394
|
+
throw new OxygenError("invalid_request", "Pass one phone or --phones.", { exitCode: 2 });
|
|
395
|
+
return options.phones !== undefined
|
|
396
|
+
? requestOxygen("/api/cli/suppressions/phones", { method: "DELETE", body: { phones: readSuppressionBulkKeys(options.phones, "--phones") } })
|
|
397
|
+
: requestOxygen(`/api/cli/suppressions/phones?phone=${encodeURIComponent(phone)}`, { method: "DELETE" });
|
|
398
|
+
});
|
|
399
|
+
}))
|
|
400
|
+
.addCommand(new Command("companies")
|
|
401
|
+
.description("List explicit company-domain and LinkedIn-company suppressions. These stop every Sequence channel.")
|
|
402
|
+
.option("--kind <kind>", "domain or linkedin_company.")
|
|
403
|
+
.option("--reason <reason>", "manual, complaint, policy, or do_not_contact.")
|
|
404
|
+
.option("--search <text>", "Identity substring.")
|
|
405
|
+
.option("--limit <n>", "Maximum rows (1-500).")
|
|
406
|
+
.option("--offset <n>", "Pagination offset.")
|
|
407
|
+
.option("--json", "Print a JSON envelope.")
|
|
408
|
+
.action(async (options) => {
|
|
409
|
+
await handleAsyncAction("suppressions companies", options, () => {
|
|
410
|
+
const params = suppressionListParams(options);
|
|
411
|
+
const kind = readOption(options.kind);
|
|
412
|
+
if (kind)
|
|
413
|
+
params.set("kind", kind);
|
|
414
|
+
const suffix = params.toString();
|
|
415
|
+
return requestOxygen(`/api/cli/suppressions/companies${suffix ? `?${suffix}` : ""}`);
|
|
416
|
+
});
|
|
417
|
+
}))
|
|
418
|
+
.addCommand(new Command("remove-company")
|
|
419
|
+
.description("Remove one explicit company suppression or 1-500 exact identities with --identities-json.")
|
|
420
|
+
.option("--kind <kind>", "domain or linkedin_company; use with --value.")
|
|
421
|
+
.option("--value <identity>", "One domain or canonical LinkedIn company URL.")
|
|
422
|
+
.option("--identities-json <json>", "Exact batch of {kind,value} company identities to remove (1-500); exclusive with --kind/--value.")
|
|
423
|
+
.option("--json", "Print a JSON envelope.")
|
|
424
|
+
.action(async (options) => {
|
|
425
|
+
await handleAsyncAction("suppressions remove-company", options, () => {
|
|
426
|
+
const kind = readOption(options.kind);
|
|
427
|
+
const value = readOption(options.value);
|
|
428
|
+
if (options.identitiesJson !== undefined) {
|
|
429
|
+
if (options.kind !== undefined || options.value !== undefined)
|
|
430
|
+
throw new OxygenError("invalid_request", "Pass --identities-json or --kind and --value.", { exitCode: 2 });
|
|
431
|
+
const identities = parseJsonValue(options.identitiesJson, "--identities-json");
|
|
432
|
+
if (!Array.isArray(identities) || identities.length < 1 || identities.length > 500)
|
|
433
|
+
throw new OxygenError("invalid_request", "--identities-json must contain 1-500 {kind,value} objects.", { exitCode: 2 });
|
|
434
|
+
return requestOxygen("/api/cli/suppressions/companies", { method: "DELETE", body: { identities } });
|
|
435
|
+
}
|
|
436
|
+
if (!kind || !value)
|
|
437
|
+
throw new Error("--kind and --value are required.");
|
|
438
|
+
const params = new URLSearchParams({ kind, value });
|
|
439
|
+
return requestOxygen(`/api/cli/suppressions/companies?${params.toString()}`, { method: "DELETE" });
|
|
440
|
+
});
|
|
441
|
+
}))
|
|
442
|
+
.addCommand(new Command("hubspot")
|
|
443
|
+
.description("Discover and synchronize HubSpot contact/company saved lists into the shared Oxygen DNC endpoint. The same saved lists can be imported as table rows instead — see `oxygen tables hubspot import`, which syncs a do-not-contact list first and then lands the members in a new table.")
|
|
444
|
+
.addCommand(new Command("lists")
|
|
445
|
+
.description("List contact/company saved lists and readable identity properties. Reads definitions only; no memberships or writes.")
|
|
446
|
+
.option("--query <text>", "Optional list-name search.")
|
|
447
|
+
.option("--json", "Print a JSON envelope.")
|
|
448
|
+
.action(async (options) => {
|
|
449
|
+
await handleAsyncAction("suppressions hubspot lists", options, () => {
|
|
450
|
+
const params = new URLSearchParams();
|
|
451
|
+
const query = readOption(options.query);
|
|
452
|
+
if (query)
|
|
453
|
+
params.set("query", query);
|
|
454
|
+
const suffix = params.toString();
|
|
455
|
+
return requestOxygen(`/api/cli/suppressions/hubspot-sync${suffix ? `?${suffix}` : ""}`);
|
|
456
|
+
});
|
|
457
|
+
}))
|
|
458
|
+
.addCommand(new Command("sync")
|
|
459
|
+
.description("Preview or arm additive HubSpot → DNC sync. Requires at least one contact/company list. Live requires the exact dry-run fingerprint and approval; never enrolls or sends.")
|
|
460
|
+
.option("--contact-list <id>", "Optional HubSpot contact DNC list id.")
|
|
461
|
+
.option("--company-list <id>", "Optional HubSpot company DNC list id.")
|
|
462
|
+
.option("--contact-linkedin-property <name>", "Optional contact LinkedIn profile property.")
|
|
463
|
+
.option("--company-domain-property <name>", "Company domain property (default domain).", "domain")
|
|
464
|
+
.option("--company-linkedin-property <name>", "Optional company LinkedIn URL property.")
|
|
465
|
+
.option("--cron <expression>", "Schedule; default every 15 minutes.")
|
|
466
|
+
.option("--timezone <iana>", "Schedule timezone; default UTC.")
|
|
467
|
+
.option("--max-records-per-list <n>", "Complete-snapshot cap (1-5000).", "5000")
|
|
468
|
+
.option("--max-credits <n>", "Hard credits cap per delivery.", "10")
|
|
469
|
+
.option("--reviewed-fingerprint <hash>", "Exact dry-run fingerprint, required for live.")
|
|
470
|
+
.option("--idempotency-key <key>", "Optional first-run idempotency key.")
|
|
471
|
+
.option("--live", "Arm the Workflow and queue the first durable run.")
|
|
472
|
+
.option("--approved", "Approve the reviewed bounded recurring reads and additive DNC writes.")
|
|
473
|
+
.option("--json", "Print a JSON envelope.")
|
|
474
|
+
.action(async (options) => {
|
|
475
|
+
await handleAsyncAction("suppressions hubspot sync", options, () => {
|
|
476
|
+
const contactList = readOption(options.contactList);
|
|
477
|
+
const companyList = readOption(options.companyList);
|
|
478
|
+
if (!contactList && !companyList)
|
|
479
|
+
throw new Error("Pass --contact-list and/or --company-list.");
|
|
480
|
+
const maxRecords = readPositiveInt(options.maxRecordsPerList);
|
|
481
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
482
|
+
if (maxRecords === undefined || maxRecords > 5000)
|
|
483
|
+
throw new Error("--max-records-per-list must be between 1 and 5000.");
|
|
484
|
+
if (maxCredits === undefined)
|
|
485
|
+
throw new Error("--max-credits must be positive.");
|
|
486
|
+
const fingerprint = readOption(options.reviewedFingerprint);
|
|
487
|
+
if (options.live && !fingerprint)
|
|
488
|
+
throw new Error("Live activation requires --reviewed-fingerprint from the exact dry-run.");
|
|
489
|
+
return requestOxygen("/api/cli/suppressions/hubspot-sync", {
|
|
490
|
+
method: "POST",
|
|
491
|
+
body: {
|
|
492
|
+
contact_list_id: contactList ?? null,
|
|
493
|
+
company_list_id: companyList ?? null,
|
|
494
|
+
mode: options.live ? "live" : "dry_run",
|
|
495
|
+
max_records_per_list: maxRecords,
|
|
496
|
+
max_credits: maxCredits,
|
|
497
|
+
...(readOption(options.contactLinkedinProperty) ? { contact_linkedin_property: readOption(options.contactLinkedinProperty) } : {}),
|
|
498
|
+
...(readOption(options.companyDomainProperty) ? { company_domain_property: readOption(options.companyDomainProperty) } : {}),
|
|
499
|
+
...(readOption(options.companyLinkedinProperty) ? { company_linkedin_property: readOption(options.companyLinkedinProperty) } : {}),
|
|
500
|
+
...(readOption(options.cron) ? { cron: readOption(options.cron) } : {}),
|
|
501
|
+
...(readOption(options.timezone) ? { timezone: readOption(options.timezone) } : {}),
|
|
502
|
+
...(fingerprint ? { reviewed_fingerprint: fingerprint } : {}),
|
|
503
|
+
...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
|
|
504
|
+
...(options.approved ? { approved: true } : {}),
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
})))
|
|
509
|
+
.addCommand(addSuppressionListFlags(new Command("domains")
|
|
510
|
+
.description("List the org's whole-domain email blocks, newest first. Filter by --reason (manual | complaint | policy) or --search (case-insensitive substring of the domain)."), "domains")
|
|
511
|
+
.option("--json", "Print a JSON envelope.")
|
|
512
|
+
.action(async (options) => {
|
|
513
|
+
await handleAsyncAction("suppressions domains", options, () => {
|
|
514
|
+
const suffix = suppressionListQuery(options, "domains");
|
|
515
|
+
return requestOxygen(`/api/cli/suppressions/domains${suffix ? `?${suffix}` : ""}`);
|
|
516
|
+
});
|
|
517
|
+
}))
|
|
518
|
+
.addCommand(addSuppressionListFlags(new Command("addresses")
|
|
519
|
+
.description("List the org's blocked email ADDRESSES, newest first (bounces, unsubscribes, and `suppressions import` land here). Filter by --reason (hard_bounce | complaint | unsubscribe | manual) or --search (case-insensitive substring of the address). Rows carry a CRM person name when one matches."), "addresses")
|
|
520
|
+
.option("--json", "Print a JSON envelope.")
|
|
521
|
+
.action(async (options) => {
|
|
522
|
+
await handleAsyncAction("suppressions addresses", options, () => {
|
|
523
|
+
const suffix = suppressionListQuery(options, "addresses");
|
|
524
|
+
return requestOxygen(`/api/cli/suppressions/addresses${suffix ? `?${suffix}` : ""}`);
|
|
525
|
+
});
|
|
526
|
+
}))
|
|
527
|
+
.addCommand(new Command("remove-domain")
|
|
528
|
+
.description("Remove one whole-domain email block or 1-500 with --domains immediately; this can restore sending eligibility. There is no preview or --approved flag: approve the exact domains before running. Requires workspace membership and subscription execution access; restricted client logins are denied.")
|
|
529
|
+
.argument("[domain]", "One bare domain to unblock; exclusive with --domains.")
|
|
530
|
+
.option("--domains <domains>", "Comma-separated domains to remove (1-500).")
|
|
531
|
+
.option("--json", "Print a JSON envelope.")
|
|
532
|
+
.action(async (domain, options) => {
|
|
533
|
+
await handleAsyncAction("suppressions remove-domain", options, () => {
|
|
534
|
+
const value = domain?.trim();
|
|
535
|
+
if (options.domains !== undefined) {
|
|
536
|
+
if (value)
|
|
537
|
+
throw new OxygenError("invalid_request", "Pass one domain or --domains.", { exitCode: 2 });
|
|
538
|
+
return requestOxygen("/api/cli/suppressions/domains", { method: "DELETE", body: { domains: readSuppressionBulkKeys(options.domains, "--domains") } });
|
|
539
|
+
}
|
|
540
|
+
if (!value)
|
|
541
|
+
throw new Error("A domain is required.");
|
|
542
|
+
return requestOxygen(`/api/cli/suppressions/domains?domain=${encodeURIComponent(value)}`, {
|
|
543
|
+
method: "DELETE",
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
}))
|
|
547
|
+
.addCommand(new Command("remove-address")
|
|
548
|
+
.description("Remove one blocked email address or 1-500 with --emails immediately; this can restore sending eligibility. Whole-domain blocks use remove-domain. There is no preview or --approved flag: approve the exact addresses before running. Requires workspace membership and subscription execution access; restricted client logins are denied.")
|
|
549
|
+
.argument("[email]", "One blocked address, e.g. ada@acme.com; exclusive with --emails.")
|
|
550
|
+
.option("--emails <emails>", "Comma-separated exact email addresses to remove (1-500).")
|
|
551
|
+
.option("--json", "Print a JSON envelope.")
|
|
552
|
+
.action(async (email, options) => {
|
|
553
|
+
const emails = options.emails;
|
|
554
|
+
if (emails !== undefined) {
|
|
555
|
+
await handleAsyncAction("suppressions remove-address", options, () => {
|
|
556
|
+
if (email)
|
|
557
|
+
throw new OxygenError("invalid_request", "Pass one email or --emails.", { exitCode: 2 });
|
|
558
|
+
return requestOxygen("/api/cli/suppressions/addresses", { method: "DELETE", body: { emails: readSuppressionBulkKeys(emails, "--emails") } });
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
if (!email)
|
|
564
|
+
throw new OxygenError("invalid_request", "Pass one email or --emails.", { exitCode: 2 });
|
|
565
|
+
const data = await requestOxygen(`/api/cli/suppressions/addresses?email=${encodeURIComponent(email)}`, { method: "DELETE" });
|
|
566
|
+
if (options.json) {
|
|
567
|
+
writeJson(success("suppressions remove-address", data));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const link = data.deepLink ?? data.web_url;
|
|
571
|
+
process.stdout.write(`${data.removed ? "Removed" : "Not found:"} ${data.email ?? email}.${link ? ` ${link}` : ""}\n`);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
emitCliFailure("suppressions remove-address", error);
|
|
575
|
+
}
|
|
576
|
+
})));
|
|
577
|
+
}
|