@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,1290 @@
|
|
|
1
|
+
import { emitCliFailure, emitSuccess, writeBillingNotices } from "../cli-output.js";
|
|
2
|
+
import { readNonNegativeInt, readPositiveInt } from "../cli-values.js";
|
|
3
|
+
import { readCsvOption, readNonNegativeNumber, splitCommaList } from "../command-support.js";
|
|
4
|
+
import { requestOxygen } from "../http-client.js";
|
|
5
|
+
import { readOption } from "../util.js";
|
|
6
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
7
|
+
import { inferRowsFileFormat, parseXlsxWorkbookBuffer } from "@oxygen/shared/file-import";
|
|
8
|
+
import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation } from "@oxygen/shared/mailbox-import";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { basename, resolve } from "node:path";
|
|
12
|
+
/**
|
|
13
|
+
* The active warm-up filter as the flags that produced it, or null for a
|
|
14
|
+
* pool-wide read.
|
|
15
|
+
*
|
|
16
|
+
* The server's echo wins because it is what was actually applied; the flags this
|
|
17
|
+
* CLI sent are the fallback for a server too old to echo anything. Neither path
|
|
18
|
+
* invents a number — the choice here is only about WORDING, because the counts
|
|
19
|
+
* are already scoped to the matches and printing them bare reads as the whole
|
|
20
|
+
* workspace.
|
|
21
|
+
*/
|
|
22
|
+
function describeMailboxFilters(echo, filterFlags) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
if (echo) {
|
|
25
|
+
// Query order, so the line reads back as the command that produced it.
|
|
26
|
+
if (typeof echo.status === "string" && echo.status.length > 0) {
|
|
27
|
+
parts.push(`--status ${echo.status}`);
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(echo.tag) && echo.tag.length > 0)
|
|
30
|
+
parts.push(`--tag ${echo.tag.join(",")}`);
|
|
31
|
+
if (Array.isArray(echo.warmup) && echo.warmup.length > 0) {
|
|
32
|
+
parts.push(`--warmup ${echo.warmup.join(",")}`);
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(echo.next_action) && echo.next_action.length > 0) {
|
|
35
|
+
parts.push(`--next-action ${echo.next_action.join(",")}`);
|
|
36
|
+
}
|
|
37
|
+
if (echo.stalled === true)
|
|
38
|
+
parts.push("--stalled");
|
|
39
|
+
}
|
|
40
|
+
if (parts.length === 0)
|
|
41
|
+
parts.push(...filterFlags);
|
|
42
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
43
|
+
}
|
|
44
|
+
const MAILBOX_POOL_COLUMNS = 80;
|
|
45
|
+
/**
|
|
46
|
+
* How many mailboxes may owe an action before a block each stops being a list
|
|
47
|
+
* and becomes a wall. Measured on dev's eval workspace: 223 of 224 mailboxes
|
|
48
|
+
* needed one, and a block each printed ~1,100 lines in which the same four
|
|
49
|
+
* sentences repeated 93 times and the single unusual seat was buried in the
|
|
50
|
+
* middle of them. Under this limit the per-mailbox detail IS the value, so it
|
|
51
|
+
* stays exactly as it is; over it, the same rows are grouped by the step they
|
|
52
|
+
* need and the filter that selects each group is printed instead.
|
|
53
|
+
*/
|
|
54
|
+
const MAILBOX_POOL_DETAIL_LIMIT = 12;
|
|
55
|
+
/** Example addresses per group, and rows sampled from a long settled list. */
|
|
56
|
+
const MAILBOX_POOL_SAMPLES = 3;
|
|
57
|
+
/** Distinct cause sentences named before the rest are counted off. */
|
|
58
|
+
const MAILBOX_POOL_CAUSE_SAMPLES = 3;
|
|
59
|
+
/**
|
|
60
|
+
* Wrap prose under a label, hanging-indented to the label's width. Commands are
|
|
61
|
+
* never passed through here.
|
|
62
|
+
*/
|
|
63
|
+
function writeWrappedPoolProse(out, prefix, text,
|
|
64
|
+
// Defaults to the label's own width; the headline overrides it, because a
|
|
65
|
+
// continuation starting at column 0 with `--next-action` reads as a separate
|
|
66
|
+
// command rather than the rest of a sentence.
|
|
67
|
+
hangingIndent) {
|
|
68
|
+
const [first, ...rest] = text.split(/\s+/).filter(Boolean);
|
|
69
|
+
if (!first)
|
|
70
|
+
return;
|
|
71
|
+
const indent = hangingIndent ?? " ".repeat(prefix.length);
|
|
72
|
+
let line = `${prefix}${first}`;
|
|
73
|
+
for (const word of rest) {
|
|
74
|
+
if (line.length + 1 + word.length > MAILBOX_POOL_COLUMNS) {
|
|
75
|
+
out(line);
|
|
76
|
+
line = `${indent}${word}`;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
line = `${line} ${word}`;
|
|
80
|
+
}
|
|
81
|
+
out(line);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* `warmupNeedsAction`, read off the wire instead of recomputed: the server
|
|
85
|
+
* publishes the code, the CLI only compares it. A row from a server that sends no
|
|
86
|
+
* warmupTruth cannot owe an action this renderer could name, so it reads as
|
|
87
|
+
* settled rather than being invented into the urgent list.
|
|
88
|
+
*/
|
|
89
|
+
function mailboxNeedsWarmupAction(row) {
|
|
90
|
+
const code = row.warmupTruth?.next_action?.code;
|
|
91
|
+
return typeof code === "string" && code !== "none";
|
|
92
|
+
}
|
|
93
|
+
function mailboxPoolRow(row) {
|
|
94
|
+
const address = row.emailAddress ?? row.id ?? "(unnamed mailbox)";
|
|
95
|
+
const truth = row.warmupTruth;
|
|
96
|
+
const state = truth?.state ?? row.warmupState ?? "unknown";
|
|
97
|
+
const dispatch = truth?.dispatch;
|
|
98
|
+
const facts = [];
|
|
99
|
+
if (typeof dispatch?.current_day === "number")
|
|
100
|
+
facts.push(`day ${dispatch.current_day}`);
|
|
101
|
+
// A measured 0 is the entire story of a billed, silent seat, so it is printed.
|
|
102
|
+
// A null count was never reported by the rail and must not be shown as zero.
|
|
103
|
+
if (typeof dispatch?.sent === "number") {
|
|
104
|
+
facts.push(`${dispatch.sent.toLocaleString("en-US")} sent`);
|
|
105
|
+
}
|
|
106
|
+
// Only when it is not the boring answer: a mailbox OXYGEN is not sending from
|
|
107
|
+
// at all explains a silent warm-up faster than any warm-up field can.
|
|
108
|
+
if (typeof row.status === "string" && row.status !== "active")
|
|
109
|
+
facts.push(row.status);
|
|
110
|
+
return ` ${address.padEnd(34)} ${state.padEnd(12)} ${facts.join(" ")}`.trimEnd();
|
|
111
|
+
}
|
|
112
|
+
/** One mailbox's block: what it is, why, who stopped it, and what to run. */
|
|
113
|
+
function writeMailboxActionDetail(out, row) {
|
|
114
|
+
out(mailboxPoolRow(row));
|
|
115
|
+
const truth = row.warmupTruth;
|
|
116
|
+
if (truth?.label)
|
|
117
|
+
writeWrappedPoolProse(out, " Why: ", truth.label);
|
|
118
|
+
const pauseLabel = truth?.pause?.paused === true ? truth.pause.label : null;
|
|
119
|
+
// Who stopped it, when the state sentence did not already say so.
|
|
120
|
+
if (pauseLabel && !(truth?.label ?? "").includes(pauseLabel)) {
|
|
121
|
+
writeWrappedPoolProse(out, " Stop: ", pauseLabel);
|
|
122
|
+
}
|
|
123
|
+
const action = truth?.next_action;
|
|
124
|
+
if (action?.label)
|
|
125
|
+
writeWrappedPoolProse(out, " Fix: ", action.label);
|
|
126
|
+
if (action?.command)
|
|
127
|
+
out(` Run: ${action.command}`);
|
|
128
|
+
out();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* One mailbox's next command with its own address replaced by a placeholder, so
|
|
132
|
+
* 93 rows that differ only by recipient collapse to one printable shape instead
|
|
133
|
+
* of 93 near-identical lines.
|
|
134
|
+
*/
|
|
135
|
+
function mailboxCommandShape(row) {
|
|
136
|
+
const command = row.warmupTruth?.next_action?.command;
|
|
137
|
+
if (typeof command !== "string" || command.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
const address = row.emailAddress;
|
|
140
|
+
return address ? command.replaceAll(address, "<address>") : command;
|
|
141
|
+
}
|
|
142
|
+
function groupMailboxesByNextAction(rows) {
|
|
143
|
+
const groups = new Map();
|
|
144
|
+
for (const row of rows) {
|
|
145
|
+
const action = row.warmupTruth?.next_action;
|
|
146
|
+
const code = typeof action?.code === "string" ? action.code : "unknown";
|
|
147
|
+
const group = groups.get(code) ?? { code, rows: [], causes: [], commands: [] };
|
|
148
|
+
group.rows.push(row);
|
|
149
|
+
// Collected, never merged: two mailboxes can need the same step for
|
|
150
|
+
// different reasons, and printing one of those reasons over both is a lie
|
|
151
|
+
// the reader would act on.
|
|
152
|
+
if (action?.label && !group.causes.includes(action.label))
|
|
153
|
+
group.causes.push(action.label);
|
|
154
|
+
const shape = mailboxCommandShape(row);
|
|
155
|
+
if (shape && !group.commands.includes(shape))
|
|
156
|
+
group.commands.push(shape);
|
|
157
|
+
groups.set(code, group);
|
|
158
|
+
}
|
|
159
|
+
// Biggest first — the wall is what most needs collapsing — and alphabetically
|
|
160
|
+
// inside a tie so two runs over the same pool read the same way.
|
|
161
|
+
return [...groups.values()].sort((left, right) => right.rows.length - left.rows.length || left.code.localeCompare(right.code));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Example addresses, fitted to the line rather than counted out blindly: the
|
|
165
|
+
* trailing "(+N more)" is part of the budget, so this line cannot be the one
|
|
166
|
+
* that breaks the 80-column promise.
|
|
167
|
+
*/
|
|
168
|
+
function mailboxSampleLine(rows, prefix) {
|
|
169
|
+
const addresses = rows.map((row) => row.emailAddress ?? row.id ?? "(unnamed mailbox)");
|
|
170
|
+
const shown = [];
|
|
171
|
+
for (const address of addresses.slice(0, MAILBOX_POOL_SAMPLES)) {
|
|
172
|
+
const remaining = addresses.length - (shown.length + 1);
|
|
173
|
+
const suffix = remaining > 0 ? ` (+${remaining} more)` : "";
|
|
174
|
+
const candidate = `${prefix}${[...shown, address].join(", ")}${suffix}`;
|
|
175
|
+
if (candidate.length > MAILBOX_POOL_COLUMNS && shown.length > 0)
|
|
176
|
+
break;
|
|
177
|
+
shown.push(address);
|
|
178
|
+
}
|
|
179
|
+
const remaining = addresses.length - shown.length;
|
|
180
|
+
return `${prefix}${shown.join(", ")}${remaining > 0 ? ` (+${remaining} more)` : ""}`;
|
|
181
|
+
}
|
|
182
|
+
/** The opening sentence, marked as clipped when there was more after it. */
|
|
183
|
+
function firstSentenceOf(text) {
|
|
184
|
+
const [first] = text.split(/(?<=[.!?])\s+/);
|
|
185
|
+
if (!first || first.length === text.length)
|
|
186
|
+
return text;
|
|
187
|
+
// The clipped sentence keeps its own question or exclamation mark; a full stop
|
|
188
|
+
// becomes the ellipsis, so the line never reads ". ...".
|
|
189
|
+
return `${first.replace(/\.$/, "")}\u2026`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* `listAll` is the other side of the `List:` line this function prints: once the
|
|
193
|
+
* reader has narrowed to one group, the samples they were given to choose with
|
|
194
|
+
* are no longer the answer — the addresses are. So a single group states its
|
|
195
|
+
* shared cause and command once, then lists every member compactly, and drops
|
|
196
|
+
* the filter line that would only point back at the view they are already in.
|
|
197
|
+
*/
|
|
198
|
+
function writeMailboxActionGroups(out, groups, listAll = false) {
|
|
199
|
+
for (const group of groups) {
|
|
200
|
+
const count = group.rows.length;
|
|
201
|
+
const [only] = group.rows;
|
|
202
|
+
const noun = `${count} mailbox${count === 1 ? "" : "es"}`;
|
|
203
|
+
// A group of one is the odd seat in a pool of hundreds — the one thing a
|
|
204
|
+
// wall of repeated blocks buries. Name it on its own header line.
|
|
205
|
+
out(count === 1 && only
|
|
206
|
+
? ` ${group.code} ${noun}: ${only.emailAddress ?? only.id ?? "(unnamed mailbox)"}`
|
|
207
|
+
: ` ${group.code} ${noun}`);
|
|
208
|
+
const [firstCause, ...otherCauses] = group.causes;
|
|
209
|
+
if (firstCause && otherCauses.length === 0) {
|
|
210
|
+
writeWrappedPoolProse(out, " Why: ", firstCause);
|
|
211
|
+
}
|
|
212
|
+
else if (firstCause) {
|
|
213
|
+
out(` Why: ${group.causes.length} different causes share this next step:`);
|
|
214
|
+
for (const cause of group.causes.slice(0, MAILBOX_POOL_CAUSE_SAMPLES)) {
|
|
215
|
+
// Opening sentence only: what differs between two causes sharing one
|
|
216
|
+
// next step is stated up front (the domain, the fault), and the rest is
|
|
217
|
+
// the shared remedy repeated verbatim. The full text is one `List:` line
|
|
218
|
+
// away, printed directly below.
|
|
219
|
+
writeWrappedPoolProse(out, " - ", firstSentenceOf(cause));
|
|
220
|
+
}
|
|
221
|
+
if (group.causes.length > MAILBOX_POOL_CAUSE_SAMPLES) {
|
|
222
|
+
out(` ... ${group.causes.length - MAILBOX_POOL_CAUSE_SAMPLES} more distinct causes`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// One member means the real command; more means the shape, because a
|
|
226
|
+
// placeholder is honest about the 92 addresses it stands for.
|
|
227
|
+
const single = count === 1 && only ? only.warmupTruth?.next_action?.command : null;
|
|
228
|
+
const commands = single ? [single] : group.commands;
|
|
229
|
+
for (const command of commands.slice(0, 2))
|
|
230
|
+
out(` Run: ${command}`);
|
|
231
|
+
if (commands.length > 2) {
|
|
232
|
+
const rest = commands.length - 2;
|
|
233
|
+
out(` ... ${rest} more command ${rest === 1 ? "shape" : "shapes"} in this group`);
|
|
234
|
+
}
|
|
235
|
+
if (listAll) {
|
|
236
|
+
out();
|
|
237
|
+
for (const row of group.rows)
|
|
238
|
+
out(mailboxPoolRow(row));
|
|
239
|
+
out();
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (count > 1)
|
|
243
|
+
out(mailboxSampleLine(group.rows, " e.g. "));
|
|
244
|
+
// The exact filter that turns this group back into every one of its members.
|
|
245
|
+
out(` List: oxygen mailboxes list --next-action ${group.code}`);
|
|
246
|
+
out();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function writeMailboxPoolOverview(data, view) {
|
|
250
|
+
const out = (line = "") => process.stdout.write(`${line}\n`);
|
|
251
|
+
const rows = data.mailboxes ?? [];
|
|
252
|
+
const total = typeof data.count === "number" ? data.count : rows.length;
|
|
253
|
+
const warmup = data.summary?.warmup;
|
|
254
|
+
const filterLabel = describeMailboxFilters(data.filters, view.filterFlags);
|
|
255
|
+
if (filterLabel) {
|
|
256
|
+
// count, summary.total and summary.warmup are all scoped to what matched, so
|
|
257
|
+
// the pool-wide headline would be right about the wrong population: "2
|
|
258
|
+
// mailboxes" to a founder with 224 inboxes is not a smaller number, it is a
|
|
259
|
+
// false one. Name the filter, count the matches, and say which is which.
|
|
260
|
+
const stalled = warmup?.stalled;
|
|
261
|
+
const clause = typeof stalled === "number" && stalled > 0 && stalled < total
|
|
262
|
+
? ` — ${stalled} of them have never sent`
|
|
263
|
+
: "";
|
|
264
|
+
writeWrappedPoolProse(out, "", `${total} mailbox${total === 1 ? "" : "es"} match${total === 1 ? "es" : ""} ${filterLabel}${clause}`, " ");
|
|
265
|
+
out("(a filtered view — not your whole pool; run oxygen mailboxes list for that)");
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const headline = [`${total} mailbox${total === 1 ? "" : "es"}`];
|
|
269
|
+
if (typeof warmup?.enrolled === "number")
|
|
270
|
+
headline.push(`${warmup.enrolled} in warm-up`);
|
|
271
|
+
if (typeof warmup?.stalled === "number") {
|
|
272
|
+
headline.push(`${warmup.stalled} enrolled but never sent`);
|
|
273
|
+
}
|
|
274
|
+
out(headline.join(" "));
|
|
275
|
+
}
|
|
276
|
+
// An older server has no summary.warmup at all. Say that under either
|
|
277
|
+
// headline, rather than printing a 0 a customer would read as "nothing is
|
|
278
|
+
// stalled" — or, in the filtered view, silently omitting the one count that
|
|
279
|
+
// explains why no stalled clause appeared.
|
|
280
|
+
if (!warmup)
|
|
281
|
+
out("(this server does not report warm-up pool counts)");
|
|
282
|
+
// Above the rows, like NEEDS ACTION: mailboxes that stopped sending because
|
|
283
|
+
// their monthly credits were not covered, and the date they disconnect.
|
|
284
|
+
if (data.reservations_paused?.message) {
|
|
285
|
+
out();
|
|
286
|
+
writeWrappedPoolProse(out, "PAUSED ", data.reservations_paused.message, " ");
|
|
287
|
+
}
|
|
288
|
+
out();
|
|
289
|
+
if (rows.length === 0) {
|
|
290
|
+
out(view.filtered ? "No mailbox matches that filter." : "No mailboxes in this workspace yet.");
|
|
291
|
+
const emptyLink = data.web_url ?? data.deepLink;
|
|
292
|
+
if (emptyLink) {
|
|
293
|
+
out();
|
|
294
|
+
out(emptyLink);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const needsAction = rows.filter((row) => mailboxNeedsWarmupAction(row));
|
|
299
|
+
const settled = rows.filter((row) => !mailboxNeedsWarmupAction(row));
|
|
300
|
+
// First, and never behind the healthy ones: this block is the reason the
|
|
301
|
+
// command was run.
|
|
302
|
+
if (needsAction.length > 0 && needsAction.length <= MAILBOX_POOL_DETAIL_LIMIT) {
|
|
303
|
+
out(`NEEDS ACTION (${needsAction.length})`);
|
|
304
|
+
out();
|
|
305
|
+
for (const row of needsAction)
|
|
306
|
+
writeMailboxActionDetail(out, row);
|
|
307
|
+
}
|
|
308
|
+
else if (needsAction.length > 0) {
|
|
309
|
+
const groups = groupMailboxesByNextAction(needsAction);
|
|
310
|
+
if (groups.length === 1) {
|
|
311
|
+
out(`NEEDS ACTION (${needsAction.length}) — all of them need the same next step`);
|
|
312
|
+
out();
|
|
313
|
+
writeMailboxActionGroups(out, groups, true);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
out(`NEEDS ACTION (${needsAction.length}) — grouped by next step, not listed one by one`);
|
|
317
|
+
out();
|
|
318
|
+
writeMailboxActionGroups(out, groups);
|
|
319
|
+
// Nothing is hidden, and the reader should not have to add the groups up
|
|
320
|
+
// to be sure of it.
|
|
321
|
+
out(` All ${needsAction.length} are in a group above. Add --json for every row.`);
|
|
322
|
+
out();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (settled.length > 0) {
|
|
326
|
+
out(`NOTHING TO DO (${settled.length})`);
|
|
327
|
+
out();
|
|
328
|
+
// A healthy mailbox has nothing to say beyond existing, so past the limit a
|
|
329
|
+
// sample plus the count carries the same information as the whole list.
|
|
330
|
+
const shown = settled.length <= MAILBOX_POOL_DETAIL_LIMIT
|
|
331
|
+
? settled
|
|
332
|
+
: settled.slice(0, MAILBOX_POOL_SAMPLES);
|
|
333
|
+
for (const row of shown)
|
|
334
|
+
out(mailboxPoolRow(row));
|
|
335
|
+
if (shown.length < settled.length) {
|
|
336
|
+
out(` ... ${settled.length - shown.length} more, not listed`);
|
|
337
|
+
}
|
|
338
|
+
out();
|
|
339
|
+
}
|
|
340
|
+
const link = data.web_url ?? data.deepLink;
|
|
341
|
+
if (link)
|
|
342
|
+
out(link);
|
|
343
|
+
if (!view.filtered && needsAction.length > 0) {
|
|
344
|
+
out("Narrow it with --stalled, --warmup <state>, or --next-action <code>.");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Whitelist mailbox import fields before the local file crosses the network.
|
|
349
|
+
* In particular, a credential cannot be silently sent through the ordinary
|
|
350
|
+
* inline path: the caller must choose --from credentials so the server applies
|
|
351
|
+
* the encrypted transfer-vault contract. Historical aliases remain accepted by
|
|
352
|
+
* the parser for backward compatibility but are not advertised.
|
|
353
|
+
*/
|
|
354
|
+
function normalizeMailboxImportFile(value, mode) {
|
|
355
|
+
try {
|
|
356
|
+
return normalizeSharedMailboxImportFile(value, mode);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (error instanceof OxygenError &&
|
|
360
|
+
error.message ===
|
|
361
|
+
"This identity-only import contains a password. Choose a secure credential import for a compatible Google app-password export.") {
|
|
362
|
+
throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source>; keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function summarizeMailboxImportValidation(mailboxes, input) {
|
|
368
|
+
return {
|
|
369
|
+
...summarizeSharedMailboxImportValidation(mailboxes, input),
|
|
370
|
+
next_action: "Re-run the same command without --validate-only to register these mailboxes.",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function normalizeMailboxImportVendor(raw, from) {
|
|
374
|
+
return normalizeSharedMailboxImportVendor(raw, from);
|
|
375
|
+
}
|
|
376
|
+
async function readMailboxImportFile(path) {
|
|
377
|
+
let buffer;
|
|
378
|
+
try {
|
|
379
|
+
buffer = readFileSync(path);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable. CSV/JSON/JSONL/XLSX identity files are accepted; credential exports require --from credentials --vendor <source>. See https://oxygen-agent.com/docs/providers/mailboxes.`, { exitCode: 1 });
|
|
383
|
+
}
|
|
384
|
+
if (buffer.byteLength > SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES) {
|
|
385
|
+
throw new OxygenError("invalid_request", `Mailbox import files must be ${SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES / 1024 / 1024} MB or smaller.`, { exitCode: 1 });
|
|
386
|
+
}
|
|
387
|
+
const format = inferRowsFileFormat(path);
|
|
388
|
+
let rows;
|
|
389
|
+
if (format === "xlsx") {
|
|
390
|
+
try {
|
|
391
|
+
rows = normalizeMailboxWorkbookRows(await parseXlsxWorkbookBuffer(buffer));
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
throw new OxygenError("invalid_mailbox_import_file", `Couldn't parse the ${format.toUpperCase()} mailbox file. File contents are omitted from this error because mailbox imports may contain credentials.`, { exitCode: 1 });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
rows = parseMailboxImportText(buffer.toString("utf8"), format);
|
|
399
|
+
}
|
|
400
|
+
if (rows.length > SHARED_MAILBOX_IMPORT_ROW_LIMIT) {
|
|
401
|
+
throw new OxygenError("invalid_request", `Mailbox imports are limited to ${SHARED_MAILBOX_IMPORT_ROW_LIMIT} rows per request.`, { exitCode: 1 });
|
|
402
|
+
}
|
|
403
|
+
return rows;
|
|
404
|
+
}
|
|
405
|
+
export function registerMailboxesFamily(program, handleAsyncAction) {
|
|
406
|
+
program.addCommand(new Command("mailboxes")
|
|
407
|
+
.description("Connect customer-owned Gmail/Microsoft 365 inboxes with `oxygen mailboxes connect-oauth --vendor composio` for mail history in Unibox and business activity in CRM; read and control progress with `oxygen mailboxes sync`. Also operate the native email sending pool: register/refresh Google/Microsoft mailboxes (including Microsoft SMTP transport and secure local credential-file transfer), pause, disable or delete inboxes, connect EmailGuard monitoring, and inspect/control OXYGEN Warm-up. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact-scope warm-up automatically after provisioning under their approved default-on add-on. Google reuses the managed credential just in time; Microsoft/Azure uses native export. Standalone warm-up plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. OXYGEN Warm-up never owns campaign dispatch. Every inbox here always belongs to a sender profile (the person it sends as) — list, create, and attach those with `oxygen senders profiles`. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
|
|
408
|
+
.addCommand(new Command("list")
|
|
409
|
+
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), worker-owned connectionRepair and warmupRepair state, and a pool overview (including counts by source). Read each mailbox's warmupTruth for warm-up (state, last_send, pause, dispatch, next_action with the exact command); warmupState is only the stored rail token and reads `error` for a provider-paused seat. mode=automatic means OXYGEN owns the next bounded retry — do not ask for browser consent or disable/re-enable an existing warm-up seat. To see only Google/Microsoft mailboxes that still need OAuth connection, including attempt budgets and manual remedies, use `oxygen mailboxes oauth-health --json`. Read the fleet from `summary` — `summary.by_warmup`, `by_status`, `by_provider`, `by_auth_mode`, `by_transport`, and `by_source` already aggregate every mailbox, so you never need to iterate the `mailboxes` array to count them. `summary.warmup` adds the normalized pool counts — by_truth_state, by_next_action, enrolled, stalled (enrolled with a measured zero warm-up sends), and needs_action — and `--warmup <states>`, `--next-action <codes>`, and `--stalled` narrow the list to exactly those mailboxes, so \"which of my inboxes are not sending\" is one command rather than a local transform over every row. Without `--json` this prints a readable pool: the counts, then every mailbox that owes an action with its cause and the exact command to run.")
|
|
410
|
+
.option("--status <status>", "Filter by status: active, paused, disabled, or provisioning (ordered, still being set up).")
|
|
411
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches mailboxes carrying ANY of these tags (see `oxygen tags list`).")
|
|
412
|
+
.option("--warmup <states>", "Show only mailboxes whose warm-up is in one of these states (comma-separated): warming, active, paused, error, disabled, pending, not_enrolled, unknown.")
|
|
413
|
+
.option("--next-action <codes>", "Show only mailboxes whose warm-up needs one of these next steps (comma-separated): none, resume_warmup, reconnect_warmup, enable_warmup, connect_oauth, fix_dns, wait, contact_support.")
|
|
414
|
+
.option("--stalled", "Show only mailboxes enrolled in warm-up with zero recorded sends (warmupTruth.dispatch.sent null or 0) — the seats you are paying for that are silent. In practice these are the `pending` truth state; an `error` seat has usually sent some mail before it stopped, so it is not stalled.")
|
|
415
|
+
.option("--json", "Print a JSON envelope. The full payload carries per-mailbox daily warm-up metrics, so a large fleet is megabytes — narrow it with --stalled / --warmup / --next-action, or read the default human view, which already aggregates the pool.")
|
|
416
|
+
.action(async (options) => {
|
|
417
|
+
const params = new URLSearchParams();
|
|
418
|
+
const status = readOption(options.status);
|
|
419
|
+
if (status)
|
|
420
|
+
params.set("status", status);
|
|
421
|
+
const tags = splitCommaList(options.tag);
|
|
422
|
+
if (tags.length > 0)
|
|
423
|
+
params.set("tag", tags.join(","));
|
|
424
|
+
const warmupStates = splitCommaList(options.warmup);
|
|
425
|
+
if (warmupStates.length > 0)
|
|
426
|
+
params.set("warmup", warmupStates.join(","));
|
|
427
|
+
const nextActions = splitCommaList(options.nextAction);
|
|
428
|
+
if (nextActions.length > 0)
|
|
429
|
+
params.set("next_action", nextActions.join(","));
|
|
430
|
+
// Only ever sent as `true`. Omitted when the flag is absent so the
|
|
431
|
+
// server never has to decide what an unasked-for `stalled=false` meant.
|
|
432
|
+
if (options.stalled)
|
|
433
|
+
params.set("stalled", "true");
|
|
434
|
+
const suffix = params.toString();
|
|
435
|
+
// What this CLI asked for, kept as flags in query order: a server too
|
|
436
|
+
// old to echo `filters` back still must not let a match count read as
|
|
437
|
+
// the whole pool. Every facet narrows the same counts, so every facet
|
|
438
|
+
// is here — status and tag included.
|
|
439
|
+
const filterFlags = [];
|
|
440
|
+
if (status)
|
|
441
|
+
filterFlags.push(`--status ${status}`);
|
|
442
|
+
if (tags.length > 0)
|
|
443
|
+
filterFlags.push(`--tag ${tags.join(",")}`);
|
|
444
|
+
if (warmupStates.length > 0)
|
|
445
|
+
filterFlags.push(`--warmup ${warmupStates.join(",")}`);
|
|
446
|
+
if (nextActions.length > 0)
|
|
447
|
+
filterFlags.push(`--next-action ${nextActions.join(",")}`);
|
|
448
|
+
if (options.stalled)
|
|
449
|
+
filterFlags.push("--stalled");
|
|
450
|
+
// Bypasses handleAsyncAction the way `tables link` does: --json stays
|
|
451
|
+
// the byte-identical envelope, and a terminal gets a pool a person can
|
|
452
|
+
// read instead of the raw blob it used to print.
|
|
453
|
+
const result = await requestOxygen(`/api/cli/mailboxes${suffix ? `?${suffix}` : ""}`).catch((error) => {
|
|
454
|
+
emitCliFailure("mailboxes list", error);
|
|
455
|
+
return null;
|
|
456
|
+
});
|
|
457
|
+
if (!result)
|
|
458
|
+
return;
|
|
459
|
+
if (options.json) {
|
|
460
|
+
writeBillingNotices("mailboxes list", result);
|
|
461
|
+
emitSuccess("mailboxes list", result, options);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
writeMailboxPoolOverview(result, { filtered: suffix.length > 0, filterFlags });
|
|
465
|
+
}))
|
|
466
|
+
.addCommand(new Command("get")
|
|
467
|
+
.description("Get one sending mailbox's configuration/readiness detail (provider, status, daily cap, warmup state, auth mode, and source — managed vs bring-your-own) plus a one-row pool summary. effective_cap comes first: today's campaign limit (effective_daily_cap), the ramp-up mode and its ramp_schedule. This is not sent/replied/bounced performance; use `oxygen sequences analytics` for native Sequence attribution by mailbox/domain. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block OXYGEN Warm-up or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
|
|
468
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
469
|
+
.option("--json", "Print a JSON envelope.")
|
|
470
|
+
.action(async (mailbox, options) => {
|
|
471
|
+
await handleAsyncAction("mailboxes get", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`));
|
|
472
|
+
}))
|
|
473
|
+
.addCommand(new Command("delete")
|
|
474
|
+
.description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops any separately listed 100-credit connected-mailbox commitment for future renewals; the preview reports whether that commitment is active and any separately billed OXYGEN Warm-up subscription. The current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Without --cascade, a mailbox whose own warm-up or deliverability monitoring is still connected fails closed with its exact teardown steps; with --cascade, those add-ons are disconnected as part of the delete (their recurring credits stop too) and nothing is deleted unless every teardown is confirmed. A live managed DOMAIN subscription covers other mailboxes, so it fails closed unless the whole domain goes too: `oxygen domains delete <domain>`, or --include-domains when every inbox on it is selected.")
|
|
475
|
+
.requiredOption("--mailboxes <list>", "Comma-separated mailbox ids or addresses (maximum 500).")
|
|
476
|
+
.option("--approved", "Execute the fresh preview. Requires --plan-hash and --confirmation.")
|
|
477
|
+
.option("--plan-hash <hash>", "Fresh preview plan_hash.")
|
|
478
|
+
.option("--confirmation <phrase>", "Exact confirmation_phrase returned by the fresh preview.")
|
|
479
|
+
.option("--cascade", "Disconnect each mailbox's OWN warm-up and deliverability-monitoring add-ons as part of the delete, instead of failing closed until you tear them down by hand. Stops their recurring credits too. Pass it on the preview as well as the approval: the plan hash is bound to it, so an approval cannot turn it on. A live managed domain subscription still fails closed.")
|
|
480
|
+
.option("--domains <list>", "Exact domains to cancel/archive with this mailbox deletion; included in the preview and approval hash.")
|
|
481
|
+
.option("--include-domains", "Also delete each domain whose inboxes are ALL selected: an OXYGEN-bought domain is cancelled, your own is archived (DNS and registrar untouched). Partly selected domains are kept and listed in preview.domains_kept. Pass it on the preview as well as the approval.")
|
|
482
|
+
.option("--json", "Print a JSON envelope.")
|
|
483
|
+
.action(async (options) => {
|
|
484
|
+
await handleAsyncAction("mailboxes delete", options, () => {
|
|
485
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
486
|
+
if (mailboxes.length === 0) {
|
|
487
|
+
throw new Error("--mailboxes must contain at least one mailbox id or address.");
|
|
488
|
+
}
|
|
489
|
+
const planHash = readOption(options.planHash);
|
|
490
|
+
const confirmation = readOption(options.confirmation);
|
|
491
|
+
if (options.approved === true && (!planHash || !confirmation)) {
|
|
492
|
+
throw new Error("--approved requires --plan-hash and --confirmation from a fresh deletion preview.");
|
|
493
|
+
}
|
|
494
|
+
return requestOxygen("/api/cli/mailboxes/delete", {
|
|
495
|
+
method: "POST",
|
|
496
|
+
body: {
|
|
497
|
+
mailboxes,
|
|
498
|
+
...(options.domains !== undefined ? { domains: readCsvOption(options.domains) } : {}),
|
|
499
|
+
...(options.approved === true ? { approved: true } : {}),
|
|
500
|
+
...(options.cascade === true ? { cascade: true } : {}),
|
|
501
|
+
...(options.includeDomains === true ? { include_domains: true } : {}),
|
|
502
|
+
...(planHash ? { plan_hash: planHash } : {}),
|
|
503
|
+
...(confirmation ? { confirmation } : {}),
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}))
|
|
508
|
+
.addCommand(new Command("health")
|
|
509
|
+
.description("Fleet email-health: the sending pool rolled up by external deliverability reputation (healthy/degraded/critical/unknown), per-mailbox scores, connected health providers, and DIRECTIONAL recommendations — plus OXYGEN's OWN evidence for the last 7 days, which needs no provider connected: the bounce notifications each mailbox received classified as provider rejection / dead address / mailbox full / delay (a Gmail reputation block shows up as provider_rejected), distinct recipients with a hard bounce in the window (legacy facts retain their first observation time), sequence sends OXYGEN logged (source=sequence only), warm-up day, health score and stop cause, each mailbox's configured daily cap, and the same rollup per sending domain ranked worst-first. Notifications whose body was never stored count as signals.dsn7d.unclassified, which means OXYGEN could not look — not that nothing was wrong. Read health_coverage to see how many mailboxes an external provider has actually scored. Start from the fleet read instead of scanning the mailbox array: warmup.byCause (why warm-up stopped, counted once for the whole pool). Every count is scoped, and the scope is stamped into the payload as signals.window (7d) and signals.sendSource (sequence sends only), on the fleet object and on every mailbox: a 0 means nothing was recorded in that window for that send source, NOT that the fleet is un-blocklisted everywhere. Pure read — 0 credits; never pauses a mailbox or changes a cap.")
|
|
510
|
+
.option("--json", "Print a JSON envelope.")
|
|
511
|
+
.action(async (options) => {
|
|
512
|
+
await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
|
|
513
|
+
}))
|
|
514
|
+
.addCommand(new Command("compatibility")
|
|
515
|
+
.description("Read-only compatibility report per mailbox — select with --mailboxes <address,…> (omit for the whole pool; filter here, not in a script): generic origin class, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, OXYGEN Warm-up path, EmailGuard monitoring path, and exact next actions. Read automatic_repair for native send and warmup_automatic_repair for an existing warm-up seat: mode=automatic means OXYGEN owns the next bounded retry. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the current public product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts a credential or calls a downstream provider.")
|
|
516
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
517
|
+
.option("--catalog-only", "Return only the bounded import-method, field, auth-boundary, and public provider catalogs; do not read or return workspace mailbox rows.")
|
|
518
|
+
.option("--json", "Print a JSON envelope.")
|
|
519
|
+
.action(async (options) => {
|
|
520
|
+
await handleAsyncAction("mailboxes compatibility", options, () => {
|
|
521
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
522
|
+
if (options.catalogOnly === true && mailboxes.length > 0) {
|
|
523
|
+
throw new OxygenError("invalid_scope", "--catalog-only cannot be combined with --mailboxes.", { exitCode: 1 });
|
|
524
|
+
}
|
|
525
|
+
const params = new URLSearchParams();
|
|
526
|
+
if (options.catalogOnly === true) {
|
|
527
|
+
params.set("catalog_only", "true");
|
|
528
|
+
}
|
|
529
|
+
if (mailboxes.length > 0) {
|
|
530
|
+
params.set("mailboxes", mailboxes.join(","));
|
|
531
|
+
}
|
|
532
|
+
const suffix = params.toString();
|
|
533
|
+
return requestOxygen(`/api/cli/mailboxes/compatibility${suffix ? `?${suffix}` : ""}`);
|
|
534
|
+
});
|
|
535
|
+
}))
|
|
536
|
+
.addCommand(new Command("import")
|
|
537
|
+
.description("Register (or refresh) sending mailboxes in bulk. Use --validate-only with a local file for a non-mutating, no-network preflight; without it, import is a 0-credit state mutation whose upsert is idempotent by mailbox address. CSV/JSON/JSONL/XLSX identity files from any vendor use --file plus optional --vendor. Compatible Google app-password exports use --from credentials --vendor <source>. Connected Zapmail inventories use --from zapmail. In credential files, app_password is Google-only; Microsoft password fields are discarded locally. OAuth grants, MFA/TOTP seeds, and delegation keys never transfer from customer files: fresh managed InboxKit Google orders reuse the vendor-held credential just in time for automatic warmup, while fresh managed Microsoft/Azure orders use native InboxKit Sequencer export; opted-out/separate managed enrollment uses standalone `warmup enable`; eligible non-InboxKit Microsoft warmup uses the Outlook OAuth fallback (`mailboxes warmup microsoft`); EmailGuard remains vendor_blocked for Microsoft. Google app passwords supplied through an authorized credential import are sent only in the request body, encrypted server-side, and never returned. Every imported inbox is attached to a sender profile: --sender <id> names the person, a row's first/last or display name finds or creates them, otherwise OXYGEN creates a profile named from the address for you to correct.")
|
|
538
|
+
.addHelpText("after", [
|
|
539
|
+
"",
|
|
540
|
+
"Ordinary identity file contract (CSV / JSON / JSONL / XLSX):",
|
|
541
|
+
" Canonical fields: email_address, provider, workspace_external_id?, infrastructure_platform?, tenant_id?, first_name?, last_name?, display_name?, sender_profile_id?. Common vendor aliases such as Email, From Email, ESP, Mailbox ID, Entra Tenant ID, Given Name, Surname, and From Name are mapped locally. Credentials are rejected.",
|
|
542
|
+
"",
|
|
543
|
+
"All local file imports:",
|
|
544
|
+
" Limits: 500 rows / 5 MB for identity and credential files.",
|
|
545
|
+
"",
|
|
546
|
+
"Credential file contract:",
|
|
547
|
+
' JSON: {"mailboxes":[{"email_address":"ada@send-acme.com","provider":"google","app_password":"<Google mailbox app password>"}]}',
|
|
548
|
+
" Only Google app passwords enter the encrypted seven-day transfer vault. Microsoft rows remain identity-only. Generic SMTP passwords and OAuth/MFA/delegation secrets are rejected.",
|
|
549
|
+
" Validation: add --validate-only to parse the complete real file and return safe aggregate counts plus the provider-specific exact-account OAuth review plan without authentication, a network request, or a workspace write. The plan groups only supplied addresses into reviews of at most 10; it never discovers a domain, and the later online preview may skip existing grants. Any parse, shape, provider, platform, tenant, secret-policy, or duplicate-conflict error rejects the entire file before the first mailbox write and names mailboxes[index]; validation-only never writes. A later import infrastructure failure may interrupt the upsert; re-run the same file because import is idempotent by address.",
|
|
550
|
+
" Docs: https://oxygen-agent.com/docs/providers/mailboxes",
|
|
551
|
+
" Skill: oxygen-email-infra (`oxygen skills install --skill oxygen-email-infra`).",
|
|
552
|
+
"",
|
|
553
|
+
].join("\n"))
|
|
554
|
+
.option("--file <path>", "Local CSV/JSON/JSONL/XLSX file. Common mailbox-vendor headers are normalized locally; only canonical allowlisted fields cross the network.")
|
|
555
|
+
.option("--from <source>", "Import source: 'credentials' for a compatible Google app-password export, or 'zapmail' to pull a connected workspace.")
|
|
556
|
+
.option("--vendor <slug>", "Non-secret source provenance (for example instantly, mailforge, or smartlead). Required with --from credentials; optional for identity files.")
|
|
557
|
+
.option("--connection <id>", "Zapmail connection id (--from zapmail). Defaults to the org's active Zapmail connection.")
|
|
558
|
+
.option("--provider <provider>", "Zapmail pool to pull (--from zapmail): google or microsoft. Zapmail's mailbox list is provider-scoped, so the Microsoft pool is only reachable with --provider microsoft; Microsoft mailboxes get their Entra tenant id stamped on import.")
|
|
559
|
+
.option("--sender <profileId>", "Sender profile every imported inbox belongs to (`oxygen senders profiles list` for ids, `create` for a new person). A row's own sender_profile_id or first/last name wins over it; without either, OXYGEN creates a profile named from the address.")
|
|
560
|
+
.option("--validate-only", "Parse, normalize, and policy-check a local file, then return non-secret aggregate counts without authentication, network access, provider calls, or workspace writes.")
|
|
561
|
+
.option("--json", "Print a JSON envelope.")
|
|
562
|
+
.action(async (options) => {
|
|
563
|
+
await handleAsyncAction("mailboxes import", options, async () => {
|
|
564
|
+
const from = readOption(options.from);
|
|
565
|
+
const filePath = readOption(options.file);
|
|
566
|
+
const vendor = readOption(options.vendor);
|
|
567
|
+
const connection = readOption(options.connection);
|
|
568
|
+
const provider = readOption(options.provider);
|
|
569
|
+
const sender = readOption(options.sender);
|
|
570
|
+
const validateOnly = options.validateOnly === true;
|
|
571
|
+
if (from &&
|
|
572
|
+
from !== "zapmail" &&
|
|
573
|
+
from !== "hypertide" &&
|
|
574
|
+
from !== "credentials") {
|
|
575
|
+
throw new Error("--from must be credentials or zapmail.");
|
|
576
|
+
}
|
|
577
|
+
if (from === "zapmail") {
|
|
578
|
+
if (validateOnly) {
|
|
579
|
+
throw new Error("--validate-only applies to local --file imports and cannot be combined with --from zapmail.");
|
|
580
|
+
}
|
|
581
|
+
if (filePath) {
|
|
582
|
+
throw new Error("--file cannot be combined with --from zapmail.");
|
|
583
|
+
}
|
|
584
|
+
if (vendor) {
|
|
585
|
+
throw new Error("--vendor cannot be combined with --from zapmail; the connected integration is authoritative.");
|
|
586
|
+
}
|
|
587
|
+
return requestOxygen("/api/cli/mailboxes", {
|
|
588
|
+
method: "POST",
|
|
589
|
+
body: {
|
|
590
|
+
source: "zapmail",
|
|
591
|
+
...(connection ? { connection_id: connection } : {}),
|
|
592
|
+
...(provider ? { service_provider: provider } : {}),
|
|
593
|
+
...(sender ? { sender_profile_id: sender } : {}),
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (provider)
|
|
598
|
+
throw new Error("--provider only applies with --from zapmail (inline files carry a per-mailbox provider).");
|
|
599
|
+
if (connection) {
|
|
600
|
+
throw new Error("--connection only applies with --from zapmail.");
|
|
601
|
+
}
|
|
602
|
+
const sourceProvider = normalizeMailboxImportVendor(vendor, from);
|
|
603
|
+
if (!filePath)
|
|
604
|
+
throw new Error("Provide --file <path>, --from credentials --vendor <source> --file <path>, or --from zapmail.");
|
|
605
|
+
const mailboxes = normalizeMailboxImportFile(await readMailboxImportFile(resolve(filePath)), from === "hypertide" || from === "credentials"
|
|
606
|
+
? "credential"
|
|
607
|
+
: "identity");
|
|
608
|
+
if (validateOnly) {
|
|
609
|
+
return summarizeMailboxImportValidation(mailboxes, {
|
|
610
|
+
source: from === "hypertide"
|
|
611
|
+
? "hypertide_file"
|
|
612
|
+
: from === "credentials"
|
|
613
|
+
? "credential_file"
|
|
614
|
+
: "identity_file",
|
|
615
|
+
sourceProvider: from === "hypertide" ? "hypertide" : sourceProvider,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return requestOxygen("/api/cli/mailboxes", {
|
|
619
|
+
method: "POST",
|
|
620
|
+
body: {
|
|
621
|
+
...(from === "hypertide"
|
|
622
|
+
? { source: "hypertide" }
|
|
623
|
+
: from === "credentials"
|
|
624
|
+
? { source: "credential_file" }
|
|
625
|
+
: {}),
|
|
626
|
+
...(sourceProvider
|
|
627
|
+
? { source_provider: sourceProvider }
|
|
628
|
+
: {}),
|
|
629
|
+
...(sender ? { sender_profile_id: sender } : {}),
|
|
630
|
+
mailboxes,
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
}))
|
|
635
|
+
.addCommand(new Command("tag")
|
|
636
|
+
.description('Replace a sending mailbox\'s workspace tags (whole set; `--tags ""` clears) — e.g. pool the mailboxes behind one campaign tag. See `oxygen tags list` for the vocabulary.')
|
|
637
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
638
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
639
|
+
.option("--json", "Print a JSON envelope.")
|
|
640
|
+
.action(async (mailbox, options) => {
|
|
641
|
+
await handleAsyncAction("mailboxes tag", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/tags`, {
|
|
642
|
+
method: "POST",
|
|
643
|
+
body: { tags: splitCommaList(options.tags) },
|
|
644
|
+
}));
|
|
645
|
+
}))
|
|
646
|
+
.addCommand(new Command("status")
|
|
647
|
+
.description("Set a sending mailbox's status. Pausing/disabling takes the inbox out of the rotation pool without losing its warmup state.")
|
|
648
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
649
|
+
.requiredOption("--status <status>", "New status: active, paused, or disabled.")
|
|
650
|
+
.option("--json", "Print a JSON envelope.")
|
|
651
|
+
.action(async (mailbox, options) => {
|
|
652
|
+
await handleAsyncAction("mailboxes status", options, () => {
|
|
653
|
+
const status = readOption(options.status);
|
|
654
|
+
if (!status)
|
|
655
|
+
throw new Error("--status is required.");
|
|
656
|
+
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/status`, {
|
|
657
|
+
method: "POST",
|
|
658
|
+
body: { status },
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}))
|
|
662
|
+
.addCommand(new Command("cap")
|
|
663
|
+
.description("View and adjust a single mailbox's daily send cap (throttle a problem inbox without pausing it). The campaign ramp-up that climbs to it is `mailboxes ramp set`.")
|
|
664
|
+
.addCommand(new Command("set")
|
|
665
|
+
.description("Set one sending mailbox's daily send cap (sends per day; new mailboxes get 25). --daily-cap must be a positive whole number; `oxygen limits show` reports the maximum in force. The campaign ramp-up (standard 5, 10, then 20/day over the first nine sending days) climbs to this cap; customize or turn it off with `mailboxes ramp set`. Optional provider warm-up is configured with `mailboxes warmup config`. Consumes no Oxygen credits. <mailbox> accepts a mailbox id or email address.")
|
|
666
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
667
|
+
.requiredOption("--daily-cap <n>", "New daily send cap (a positive whole number of sends per day).")
|
|
668
|
+
.option("--json", "Print a JSON envelope.")
|
|
669
|
+
.action(async (mailbox, options) => {
|
|
670
|
+
await handleAsyncAction("mailboxes cap set", options, () => {
|
|
671
|
+
const raw = readOption(options.dailyCap);
|
|
672
|
+
if (!raw)
|
|
673
|
+
throw new Error("--daily-cap is required.");
|
|
674
|
+
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/cap`, {
|
|
675
|
+
method: "PATCH",
|
|
676
|
+
body: { daily_cap: Number(raw) },
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
})))
|
|
680
|
+
.addCommand(new Command("ramp")
|
|
681
|
+
.description("Campaign ramp-up for one mailbox: how much campaign email it may send on each of its first sending days before its daily cap. Standard is 5, 10, then 20 a day over the first nine sending days; set a custom start and increase, or turn it off for an inbox already sending elsewhere. A sending day is a completed UTC day with accepted campaign sends. Provider warm-up is separate (`mailboxes warmup config`).")
|
|
682
|
+
.addCommand(new Command("set")
|
|
683
|
+
.description("Set one mailbox's campaign ramp-up: a custom --start-per-day plus --step every --increase-every-days sending days until its daily cap, --off for the full daily cap from the first sending day, or --clear for the standard 5/10/20 ramp. Sending days are completed UTC days with accepted campaign sends. The optional --cap holds a custom ramp lower for good. effective_cap in the response reads back today's limit, the ramp_schedule and full_cap_on_sending_day. Free configuration write.")
|
|
684
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
685
|
+
.option("--start-per-day <n>", "Campaign emails allowed on the first sending day (non-negative whole number).")
|
|
686
|
+
.option("--increase-every-days <n>", "Sending days between increases (whole number >= 1).")
|
|
687
|
+
.option("--step <n>", "Emails added at each increase (non-negative whole number).")
|
|
688
|
+
.option("--cap <n>", "Optional permanent ceiling for the ramp; omit it to climb until the daily cap.")
|
|
689
|
+
.option("--off", "Turn the ramp off: the full daily cap from the first sending day.")
|
|
690
|
+
.option("--clear", "Restore the standard 5/10/20 ramp.")
|
|
691
|
+
.option("--json", "Print a JSON envelope.")
|
|
692
|
+
.action(async (mailbox, options) => {
|
|
693
|
+
await handleAsyncAction("mailboxes ramp set", options, () => {
|
|
694
|
+
const knobs = [
|
|
695
|
+
options.startPerDay,
|
|
696
|
+
options.increaseEveryDays,
|
|
697
|
+
options.step,
|
|
698
|
+
options.cap,
|
|
699
|
+
].some((value) => value !== undefined);
|
|
700
|
+
if (options.off && options.clear) {
|
|
701
|
+
throw new Error("Use --off or --clear, not both.");
|
|
702
|
+
}
|
|
703
|
+
if ((options.off || options.clear) && knobs) {
|
|
704
|
+
throw new Error("--off and --clear take no ramp numbers; drop --start-per-day, --increase-every-days, --step and --cap.");
|
|
705
|
+
}
|
|
706
|
+
let rampConfig;
|
|
707
|
+
if (options.clear) {
|
|
708
|
+
rampConfig = null;
|
|
709
|
+
}
|
|
710
|
+
else if (options.off) {
|
|
711
|
+
rampConfig = { enabled: false };
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
const startPerDay = readNonNegativeInt(options.startPerDay);
|
|
715
|
+
const increaseEveryDays = readPositiveInt(options.increaseEveryDays);
|
|
716
|
+
const step = readNonNegativeInt(options.step);
|
|
717
|
+
const cap = readNonNegativeInt(options.cap);
|
|
718
|
+
if (startPerDay === undefined ||
|
|
719
|
+
increaseEveryDays === undefined ||
|
|
720
|
+
step === undefined) {
|
|
721
|
+
throw new Error("Provide --start-per-day, --increase-every-days and --step (--cap is optional), or --off to turn the ramp off, or --clear for the standard ramp.");
|
|
722
|
+
}
|
|
723
|
+
rampConfig = {
|
|
724
|
+
start_per_day: startPerDay,
|
|
725
|
+
increase_every_days: increaseEveryDays,
|
|
726
|
+
step,
|
|
727
|
+
...(cap !== undefined ? { cap } : {}),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/ramp`, {
|
|
731
|
+
method: "PATCH",
|
|
732
|
+
body: { ramp_config: rampConfig },
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
})))
|
|
736
|
+
.addCommand(new Command("update")
|
|
737
|
+
.description("Update a sending mailbox's rich-content profile: the From display name and/or the HTML signature appended to its sends. An email that already contains the signature gets no second copy. Pass an empty string ('') to clear a field. Consumes no Oxygen credits. <mailbox> accepts a mailbox id or email address.")
|
|
738
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
739
|
+
.option("--display-name <name>", "From display name (e.g. \"Ada from Acme\"). Pass '' to clear it.")
|
|
740
|
+
.option("--signature-html <html>", "HTML signature appended to every send from this mailbox. Pass '' to clear it.")
|
|
741
|
+
.option("--signature-file <path>", "Read the HTML signature from a file instead of --signature-html.")
|
|
742
|
+
.option("--bounce-protection <json>", "Mailbox bounce policy (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this mailbox. Rates are fractions; sequence protection is separate.")
|
|
743
|
+
.option("--json", "Print a JSON envelope.")
|
|
744
|
+
.action(async (mailbox, options) => {
|
|
745
|
+
await handleAsyncAction("mailboxes update", options, () => {
|
|
746
|
+
// Detect PRESENCE (options.x !== undefined), not truthiness, so passing
|
|
747
|
+
// an empty string clears the field rather than being dropped.
|
|
748
|
+
const patch = {};
|
|
749
|
+
if (options.bounceProtection !== undefined)
|
|
750
|
+
patch.bounce_protection = JSON.parse(options.bounceProtection);
|
|
751
|
+
if (options.displayName !== undefined)
|
|
752
|
+
patch.display_name = options.displayName;
|
|
753
|
+
if (options.signatureHtml !== undefined) {
|
|
754
|
+
patch.signature_html = options.signatureHtml;
|
|
755
|
+
}
|
|
756
|
+
else if (options.signatureFile) {
|
|
757
|
+
patch.signature_html = readFileSync(resolve(options.signatureFile), "utf8");
|
|
758
|
+
}
|
|
759
|
+
if (Object.keys(patch).length === 0) {
|
|
760
|
+
throw new Error("Provide --display-name and/or --signature-html (or --signature-file).");
|
|
761
|
+
}
|
|
762
|
+
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`, {
|
|
763
|
+
method: "PATCH",
|
|
764
|
+
body: patch,
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
}))
|
|
768
|
+
.addCommand(new Command("sync")
|
|
769
|
+
.description("Read or control full Inbox, Sent and archive import for Gmail or Microsoft 365. History sync and CRM analysis are included in the mailbox’s 100-credit monthly connection. Spam, trash and drafts are excluded. Without --action, this is a free progress read; controls preview until --approved. Disconnect keeps mail and CRM records, stops sync/sends and future mailbox commitment renewals, with no current-period refund. Paid seats and add-ons retain their own billing contracts.")
|
|
770
|
+
.argument("<mailbox>", "Mailbox ID or exact email address.")
|
|
771
|
+
.option("--action <action>", "start, pause, resume or disconnect; omit to read progress.")
|
|
772
|
+
.option("--approved", "Apply the reviewed control; default is preview.")
|
|
773
|
+
.option("--dry-run", "Preview the control without changing sync.")
|
|
774
|
+
.option("--json", "Print JSON.")
|
|
775
|
+
.action(async (mailbox, options) => {
|
|
776
|
+
await handleAsyncAction("mailboxes sync", options, () => options.action
|
|
777
|
+
? requestOxygen("/api/cli/mailboxes/sync", { method: "POST", body: { mailbox, action: options.action, approved: options.approved === true, dry_run: options.dryRun === true } })
|
|
778
|
+
: requestOxygen(`/api/cli/mailboxes/sync?${new URLSearchParams({ mailbox }).toString()}`));
|
|
779
|
+
}))
|
|
780
|
+
.addCommand(new Command("connect-oauth")
|
|
781
|
+
.description("Connect Gmail/Microsoft 365 with --vendor composio to import all mail into Unibox and business activity into CRM. The listed recurring rate is 100 credits per mailbox per month including history, sync and analysis; previews cost 0, with no extra execution fee. Before consent, inspect current monthly connection prices and first-debit timing with billing commitments --view connection-prices --json. Quote a mailbox count with billing estimate --mailboxes <n> --json; the managed preview shows the listed rate; an unavailable managed Gmail connection directs you to OXYGEN support, and an unavailable managed Microsoft connection offers OXYGEN's own sign-in (--vendor oxygen --authorization-mode individual) as an explicit, sending-only choice. Managed OAuth requires provider rollout readiness and exact registered addresses, max 10. Use mailboxes sync for progress and pause/resume/disconnect. Composio uses individual owner consent only. Native --vendor oxygen retains its existing polling; full history/CRM requires managed reconnection. Warm-up retains its existing eligibility and pricing. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen requires an exact address list and never discovers a domain directory. For --vendor oxygen only, Microsoft supports --authorization-mode tenant (one administrator approval, up to 500 exact selected addresses, recommended for dedicated sending tenants) or individual (one account sign-in per mailbox, max 10 per run, recommended for ordinary company/personal mailboxes). For Microsoft's built-in roles, tenant mode requires a Global Administrator or Privileged Role Administrator because it requests the Microsoft Graph application Mail.Send and Mail.Read permissions; Application Administrator and Cloud Application Administrator cannot consent to Microsoft Graph application roles. Microsoft's application grant is tenant-wide at the provider; OXYGEN—not Microsoft—enforces the selected-address boundary at verification and send time unless the tenant separately configures Exchange Application RBAC. For non-managed Microsoft mailboxes, native-send approval does not authorize OXYGEN Warm-up: every mailbox still needs its own `mailboxes warmup microsoft` Outlook consent before standalone enrollment. Connecting authorization does not change whether a mailbox is active or disabled. Google uses individual OAuth. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. Managed authorization paths remain available through their vendor options.")
|
|
782
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
783
|
+
.option("--provider <provider>", "Mailbox provider: google or microsoft.")
|
|
784
|
+
.option("--vendor <vendor>", "Authorization path: composio for customer-owned Gmail/Microsoft 365 with managed consent, history and CRM; oxygen for native/tenant consent; zapmail (default) or inboxkit for provisioned inboxes.")
|
|
785
|
+
.option("--mailboxes <list>", "Comma-separated exact mailbox addresses. Required for vendor=composio (max 10) or vendor=oxygen (tenant mode max 500; individual mode max 10).")
|
|
786
|
+
.option("--authorization-mode <mode>", "vendor=oxygen only: auto, tenant, or individual. Tenant is Microsoft-only and never scans the directory.")
|
|
787
|
+
.option("--domains <list>", "Comma-separated sending domains to limit an inboxkit run to. Omit to cover every domain in the pool.")
|
|
788
|
+
.option("--no-canary", "inboxkit only: fan out to every eligible mailbox on a domain that has never connected one, instead of firing a single canary first.")
|
|
789
|
+
.option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection.")
|
|
790
|
+
.option("--status <id>", "Poll a previously started Zapmail, InboxKit, or OXYGEN direct-consent run.")
|
|
791
|
+
.option("--dry-run", "Preview the requested mailbox/domain scope and external writes without changing vendor or ledger state. This is the default for starts; a successful preview can still have zero eligible mailboxes, so check InboxKit candidates with oauth-health before approval.")
|
|
792
|
+
.option("--approved", "Start the reviewed authorization run. Composio creates an individual consent link for each owner, who must open it and approve access with Google or Microsoft. Native vendor=oxygen records expiring browser steps for the selected owner or tenant administrator. Starting a run does not grant mail access.")
|
|
793
|
+
.option("--json", "Print a JSON envelope.")
|
|
794
|
+
.action(async (options) => {
|
|
795
|
+
await handleAsyncAction("mailboxes connect-oauth", options, () => {
|
|
796
|
+
const vendor = readOption(options.vendor);
|
|
797
|
+
const exportId = readOption(options.status);
|
|
798
|
+
if (exportId) {
|
|
799
|
+
const params = new URLSearchParams({ export_id: exportId });
|
|
800
|
+
const pollConnection = readOption(options.connection);
|
|
801
|
+
if (pollConnection)
|
|
802
|
+
params.set("connection_id", pollConnection);
|
|
803
|
+
// Omitted, the server reads the vendor off the id's shape (numeric
|
|
804
|
+
// export id vs run uuid); this is the override for the rare id that
|
|
805
|
+
// does not look like either.
|
|
806
|
+
if (vendor)
|
|
807
|
+
params.set("vendor", vendor);
|
|
808
|
+
return requestOxygen(`/api/cli/mailboxes/connect-oauth?${params.toString()}`);
|
|
809
|
+
}
|
|
810
|
+
const provider = readOption(options.provider);
|
|
811
|
+
if (!provider) {
|
|
812
|
+
throw new Error("--provider <google|microsoft> is required (or pass --status <id> to poll a run).");
|
|
813
|
+
}
|
|
814
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
815
|
+
const domains = readCsvOption(options.domains);
|
|
816
|
+
const connection = readOption(options.connection);
|
|
817
|
+
const authorizationMode = readOption(options.authorizationMode);
|
|
818
|
+
return requestOxygen("/api/cli/mailboxes/connect-oauth", {
|
|
819
|
+
method: "POST",
|
|
820
|
+
body: {
|
|
821
|
+
provider,
|
|
822
|
+
// Every key stays absent unless it was asked for, so a run
|
|
823
|
+
// without the new flags is the request this command has always
|
|
824
|
+
// sent — the server reads an absent vendor as zapmail.
|
|
825
|
+
...(vendor ? { vendor } : {}),
|
|
826
|
+
...(authorizationMode
|
|
827
|
+
? { authorization_mode: authorizationMode }
|
|
828
|
+
: {}),
|
|
829
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
830
|
+
...(domains.length > 0 ? { domains } : {}),
|
|
831
|
+
...(options.canary === false ? { canary: false } : {}),
|
|
832
|
+
...(connection ? { connection_id: connection } : {}),
|
|
833
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
834
|
+
...(options.approved ? { approved: true } : {}),
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
}))
|
|
839
|
+
.addCommand(new Command("oauth-health")
|
|
840
|
+
.description("Show every Google/Microsoft inbox with neither a usable per-mailbox OAuth token nor verified delegation/application access. automatic_repair marks Zapmail-linked rows whose safe retries OXYGEN owns in the background; browser-consent rows remain explicit. Results name connect_oauth_oxygen for imported/manual mailboxes and connect_oauth_inboxkit for that managed path. Microsoft can use one-admin tenant authorization for exact selected addresses or individual OAuth. Read-only — 0 Oxygen credits; it can pause no mailbox and sends no email.")
|
|
841
|
+
.option("--json", "Print a JSON envelope.")
|
|
842
|
+
.action(async (options) => {
|
|
843
|
+
await handleAsyncAction("mailboxes oauth-health", options, () => requestOxygen("/api/cli/mailboxes/oauth-health"));
|
|
844
|
+
}))
|
|
845
|
+
.addCommand(new Command("emailguard")
|
|
846
|
+
.description("Assess every mailbox origin for EmailGuard and connect credential-capable Google Workspace mailboxes from managed, Zapmail, or compatible encrypted external imports. Manual/native Google rows report credential_required; Microsoft 365/Azure reports vendor_blocked. Preview never materializes a password; approved execution fetches it just in time and never prints it.")
|
|
847
|
+
.addCommand(new Command("connect")
|
|
848
|
+
.description("Preview or connect selected mailboxes to EmailGuard. Managed mode is 100 credits/inbox-month, with two placement tests per inbox per cycle included; an extra managed test costs 66 credits. The preview shows the price in force. Own-key monitoring has no monthly Oxygen fee; placement tests pay the in-force platform fee and vendor charges separately. Google Workspace requires a real app password, fetched only after approval. Microsoft 365/Azure is explicitly vendor-blocked because EmailGuard exposes neither Microsoft OAuth nor tenant consent; Oxygen never downgrades it to password auth. Starts with a 0-credit, no-provider-write preview. Account connection sends no placement probe.")
|
|
849
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
850
|
+
.option("--approved", "Perform the external EmailGuard account write.")
|
|
851
|
+
.option("--plan <hash>", "Exact hash from the fresh preview (required with --approved).")
|
|
852
|
+
.option("--max-credits <n>", "Hard first-cycle credit cap from the preview (required with --approved, including 0 for BYOK).")
|
|
853
|
+
.option("--json", "Print a JSON envelope.")
|
|
854
|
+
.action(async (options) => {
|
|
855
|
+
await handleAsyncAction("mailboxes emailguard connect", options, () => {
|
|
856
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
857
|
+
const planHash = readOption(options.plan);
|
|
858
|
+
const maxCredits = options.approved
|
|
859
|
+
? readNonNegativeNumber(options.maxCredits)
|
|
860
|
+
: undefined;
|
|
861
|
+
if (options.approved && !planHash) {
|
|
862
|
+
throw new Error("--plan <hash> from the fresh preview is required with --approved.");
|
|
863
|
+
}
|
|
864
|
+
if (options.approved && maxCredits === undefined) {
|
|
865
|
+
throw new Error("--max-credits <n> from the fresh preview is required with --approved.");
|
|
866
|
+
}
|
|
867
|
+
return requestOxygen("/api/cli/mailboxes/emailguard", {
|
|
868
|
+
method: "POST",
|
|
869
|
+
body: {
|
|
870
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
871
|
+
...(options.approved ? { approved: true } : {}),
|
|
872
|
+
...(planHash ? { plan_hash: planHash } : {}),
|
|
873
|
+
...(maxCredits !== undefined
|
|
874
|
+
? { max_credits: maxCredits }
|
|
875
|
+
: {}),
|
|
876
|
+
},
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
}))
|
|
880
|
+
.addCommand(new Command("disconnect")
|
|
881
|
+
.description("Preview or remove selected mailboxes from EmailGuard and cancel their recurring monitoring subscription. Remote absence is confirmed before billing stops.")
|
|
882
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
883
|
+
.option("--approved", "Perform the external EmailGuard account deletion and cancel billing.")
|
|
884
|
+
.option("--plan <hash>", "Exact hash from the fresh preview (required with --approved).")
|
|
885
|
+
.option("--json", "Print a JSON envelope.")
|
|
886
|
+
.action(async (options) => {
|
|
887
|
+
await handleAsyncAction("mailboxes emailguard disconnect", options, () => {
|
|
888
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
889
|
+
const planHash = readOption(options.plan);
|
|
890
|
+
if (options.approved && !planHash) {
|
|
891
|
+
throw new Error("--plan <hash> from the fresh preview is required with --approved.");
|
|
892
|
+
}
|
|
893
|
+
return requestOxygen("/api/cli/mailboxes/emailguard/disconnect", {
|
|
894
|
+
method: "POST",
|
|
895
|
+
body: {
|
|
896
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
897
|
+
...(options.approved ? { approved: true } : {}),
|
|
898
|
+
...(planHash ? { plan_hash: planHash } : {}),
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
}))
|
|
903
|
+
.addCommand(new Command("auto-enroll")
|
|
904
|
+
.description("Standing permission to connect NEW eligible mailboxes to EmailGuard automatically, so monitoring keeps up with the fleet instead of needing a manual connect per inbox. Enrolment is a RECURRING monthly charge per inbox, so arming is approval-gated and always bounded by a per-cycle credit ceiling. Without --arm/--disarm/--run this reports the current state (0 credits).")
|
|
905
|
+
.option("--arm", "Arm the standing permission. Requires --max-credits; previews unless --approved.")
|
|
906
|
+
.option("--disarm", "Turn it off. Existing subscriptions are untouched. 0 credits, no approval needed.")
|
|
907
|
+
.option("--run", "Enrol one bounded batch now.")
|
|
908
|
+
.option("--dry-run", "With --run: report what would be enrolled without connecting or charging. Mailboxes held back by OXYGEN's own managed capacity are reported once under platform_condition (owner: oxygen) rather than as per-mailbox skips; skip_summary counts the rest by reason.")
|
|
909
|
+
.option("--max-credits <n>", "Per-billing-cycle credit ceiling this permission may spend (required with --arm).")
|
|
910
|
+
.option("--approved", "Actually arm (otherwise --arm returns a preview).")
|
|
911
|
+
.option("--json", "Print a JSON envelope.")
|
|
912
|
+
.action(async (options) => {
|
|
913
|
+
await handleAsyncAction("mailboxes emailguard auto-enroll", options, () => {
|
|
914
|
+
const path = "/api/cli/mailboxes/emailguard/auto-enroll";
|
|
915
|
+
if (options.disarm) {
|
|
916
|
+
return requestOxygen(path, {
|
|
917
|
+
method: "POST",
|
|
918
|
+
body: { enabled: false },
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
if (options.arm) {
|
|
922
|
+
const ceiling = readOption(options.maxCredits);
|
|
923
|
+
if (!ceiling) {
|
|
924
|
+
throw new Error("--max-credits <n> is required with --arm: a standing permission without a ceiling would be unlimited recurring spend authority.");
|
|
925
|
+
}
|
|
926
|
+
return requestOxygen(path, {
|
|
927
|
+
method: "POST",
|
|
928
|
+
body: {
|
|
929
|
+
enabled: true,
|
|
930
|
+
max_credits_per_cycle: Number(ceiling),
|
|
931
|
+
...(options.approved ? { approved: true } : {}),
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (options.run) {
|
|
936
|
+
return requestOxygen(path, {
|
|
937
|
+
method: "POST",
|
|
938
|
+
body: {
|
|
939
|
+
run: true,
|
|
940
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return requestOxygen(path);
|
|
945
|
+
});
|
|
946
|
+
})))
|
|
947
|
+
.addCommand(new Command("delegation")
|
|
948
|
+
.description("Google sending-domain delegation only — not Microsoft OAuth and not a credential-file handoff to OXYGEN Warm-up or EmailGuard. Reports covered domains plus the exact client id/scopes; --probe mints a real token per mailbox. Read-only, sends no mail, 0 Oxygen credits.")
|
|
949
|
+
.option("--domain <domain>", "Only report this sending domain.")
|
|
950
|
+
.option("--probe", "Verify each delegated domain by minting a real delegated token per mailbox. Makes one Google token call per mailbox; still 0 Oxygen credits.")
|
|
951
|
+
.option("--json", "Print a JSON envelope.")
|
|
952
|
+
.action(async (options) => {
|
|
953
|
+
await handleAsyncAction("mailboxes delegation", options, () => {
|
|
954
|
+
const params = new URLSearchParams();
|
|
955
|
+
const domain = readOption(options.domain);
|
|
956
|
+
if (domain)
|
|
957
|
+
params.set("domain", domain);
|
|
958
|
+
if (options.probe)
|
|
959
|
+
params.set("probe", "true");
|
|
960
|
+
const query = params.toString();
|
|
961
|
+
return requestOxygen(`/api/cli/mailboxes/delegation${query ? `?${query}` : ""}`);
|
|
962
|
+
});
|
|
963
|
+
}))
|
|
964
|
+
.addCommand(new Command("warmup")
|
|
965
|
+
.description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It costs 100 credits per warming inbox per month ($1), or nothing once it is included in your own mailbox's 100-credit monthly connection (2026-09 repricing); the preview shows which applies. Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warm-up add-on activate automatically after provisioning under the approved order quote; do not run a second `warmup enable`. Google reuses the InboxKit-held credential only during activation and never stores or returns it; Microsoft/Azure uses exact native export. Standalone preview → approval is for BYOK/imported mailboxes, a managed opt-out, or later separate enrollment. InboxKit auto-export stays off for Microsoft/Azure because Oxygen submits only the exact authorized UIDs, and any non-cancelled InboxKit warm-up blocks either handoff so one mailbox cannot warm twice. OXYGEN Warm-up never owns campaign dispatch: native OXYGEN Sequences still own campaigns and sends. Eligible non-InboxKit Microsoft mailboxes use the separate `mailboxes warmup microsoft` OAuth fallback. Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down there with `warmup disable`; they are never silently moved to the current managed rail. On existing active paid seats, Google credential repair uses its own enabled-by-default switch. When the separate worker recovery mode is enforce (the unset default), Microsoft OAuth or tenant-consent seats get a credential-free vendor reactivation, and stalled dispatch or DNS readiness is rechecked. Shadow reports candidates in those enabled lanes without their vendor writes; off skips those lanes. Google credential repair can still run in shadow/off. Correct bad DNS records yourself; an active unpaused paid seat in enforce mode needs no manual resume after that correction. OXYGEN never invents consent, resumes a human or safety pause, starts a new paid enrollment, or blindly repeats an ambiguous provider write. Check `warmup status` and `mailboxes list` for the exact next action.")
|
|
966
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
967
|
+
.addCommand(new Command("enable")
|
|
968
|
+
.description("Optional provider warm-up defaults to 1/day, +1/day, max 10/day (dedicated-tenant profiles stay stricter); see `mailboxes warmup config` for the effective plan and unsupported OXYGEN Warm-up reply-rate control. This is separate from the 5/10/20 campaign ramp. STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is handed off, enrolled, or billed. The price is 100 credits per warming mailbox-month, or 0 once warm-up is included in the mailbox's 100-credit monthly connection (2026-09 repricing; the preview's included_in_connection says which, and its max_credits is then 0); the preview returns the exact first-cycle ceiling. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Gmail connected through Composio uses that connection after approval of standard mailbox access (send, read and organize), without a second Google consent. The preview names this access and disabling warm-up revokes it. Existing vendor-consent seats are preserved. Eligible InboxKit-managed Google targets use the InboxKit-held credential just in time without storing or returning it; Microsoft/Azure targets use native InboxKit Sequencer export with exact UIDs and auto-export off. Any non-cancelled InboxKit warmup must be cancelled before either handoff (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. OXYGEN Warm-up retains no campaign authority — OXYGEN Sequences own enrollment and dispatch. New enrollments only ever land on the managed OXYGEN rail; the retired TrulyInbox rail is refused here and only accepts teardown.")
|
|
969
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
970
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
971
|
+
.option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
|
|
972
|
+
.option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
|
|
973
|
+
.option("--max-credits <n>", "Hard first-cycle credit cap from the preview (required with --approved).")
|
|
974
|
+
.option("--ramp-weeks <n>", "Weeks the warm-up volume climb is spread over (default 4). A paid enrollment completes its ramp after 28 days with a provider-confirmed warm-up send, then keeps warming at 10 a day until you disable it.")
|
|
975
|
+
.option("--dry-run", "Simulate without calling the provider (mailboxes marked pending).")
|
|
976
|
+
.option("--json", "Print a JSON envelope.")
|
|
977
|
+
.action(async (options) => {
|
|
978
|
+
await handleAsyncAction("mailboxes warmup enable", options, () => {
|
|
979
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
980
|
+
const connection = readOption(options.connection);
|
|
981
|
+
const billingMode = readOption(options.billingMode);
|
|
982
|
+
const provider = readOption(options.provider);
|
|
983
|
+
const planHash = readOption(options.plan);
|
|
984
|
+
const maxCredits = options.approved
|
|
985
|
+
? readNonNegativeNumber(options.maxCredits)
|
|
986
|
+
: undefined;
|
|
987
|
+
const rampWeeks = readOption(options.rampWeeks);
|
|
988
|
+
if (options.approved && !planHash) {
|
|
989
|
+
throw new Error("--plan <hash> from the fresh preview is required with --approved.");
|
|
990
|
+
}
|
|
991
|
+
if (options.approved && maxCredits === undefined) {
|
|
992
|
+
throw new Error("--max-credits <n> from the fresh preview is required with --approved.");
|
|
993
|
+
}
|
|
994
|
+
return requestOxygen("/api/cli/mailboxes/warmup", {
|
|
995
|
+
method: "POST",
|
|
996
|
+
body: {
|
|
997
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
998
|
+
...(connection ? { connection_id: connection } : {}),
|
|
999
|
+
...(billingMode ? { billing_mode: billingMode } : {}),
|
|
1000
|
+
...(provider ? { provider } : {}),
|
|
1001
|
+
...(options.approved ? { approved: true } : {}),
|
|
1002
|
+
...(planHash ? { plan_hash: planHash } : {}),
|
|
1003
|
+
...(maxCredits !== undefined
|
|
1004
|
+
? { max_credits: maxCredits }
|
|
1005
|
+
: {}),
|
|
1006
|
+
...(rampWeeks ? { ramp_weeks: Number(rampWeeks) } : {}),
|
|
1007
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
}))
|
|
1012
|
+
.addCommand(new Command("google")
|
|
1013
|
+
.description("Legacy Google owner consent for one to ten connected inboxes. Gmail connected through Composio uses warmup enable to review standard mailbox access without a second Google consent. This legacy flow is separate from native sending: no passwords or OXYGEN tokens transfer. Preview costs 0 credits and creates no link. Approve its hash with --max-credits 0, open each consent_url as that exact inbox, then poll --status. Google shows the warm-up provider's consent application. Warmup stays off until a separately approved warmup enable; its fresh preview shows the price, including 0 extra credits when included in the mailbox connection.")
|
|
1014
|
+
.requiredOption("--mailboxes <list>", "Exact Google inbox ids or addresses, comma-separated; maximum ten.")
|
|
1015
|
+
.option("--approved", "Create the previewed authorization links.")
|
|
1016
|
+
.option("--plan <hash>", "Fresh preview hash; required with approval.")
|
|
1017
|
+
.option("--max-credits <n>", "Exactly 0; required with approval.")
|
|
1018
|
+
.option("--status", "Verify completed Google authorizations; never starts warmup.")
|
|
1019
|
+
.option("--json", "Print a JSON envelope.")
|
|
1020
|
+
.action(async (options) => {
|
|
1021
|
+
await handleAsyncAction("mailboxes warmup google", options, () => {
|
|
1022
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1023
|
+
const planHash = readOption(options.plan);
|
|
1024
|
+
const maxCredits = options.maxCredits === undefined ? undefined : readNonNegativeNumber(options.maxCredits);
|
|
1025
|
+
if (options.approved && options.status)
|
|
1026
|
+
throw new Error("Status and approval cannot be combined.");
|
|
1027
|
+
if (options.approved && (!planHash || maxCredits !== 0))
|
|
1028
|
+
throw new Error("Approval requires a fresh --plan and --max-credits 0.");
|
|
1029
|
+
return requestOxygen("/api/cli/mailboxes/warmup/google", { method: "POST", body: {
|
|
1030
|
+
mailboxes, ...(options.approved ? { approved: true } : {}), ...(options.status ? { status: true } : {}),
|
|
1031
|
+
...(planHash ? { plan_hash: planHash } : {}), ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
1032
|
+
} });
|
|
1033
|
+
});
|
|
1034
|
+
}))
|
|
1035
|
+
.addCommand(new Command("microsoft")
|
|
1036
|
+
.description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for OXYGEN Warm-up with ONE browser consent PER MAILBOX, never a password or app password. Do not run this for managed InboxKit mailboxes: fresh Microsoft/Azure orders activate native InboxKit warm-up exports automatically when the approved order includes warmup; opted-out or later separate managed enrollment uses standalone `warmup enable`. For the non-InboxKit fallback there is no tenant-admin shortcut: a 40-inbox Microsoft pool means 40 separate authorizations, each opened by whoever can sign in to that mailbox. The preview names the exact fallback mailboxes and outstanding links at 0 credits. Approve that fresh hash with --max-credits 0, open every returned consent_url, then re-run with --status. Warmup itself stays OFF until standalone `warmup enable`.")
|
|
1037
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
1038
|
+
.option("--mailboxes <list>", "Comma-separated eligible non-InboxKit Microsoft mailbox ids or addresses. Omit to select the pool; the command refuses a scope containing managed InboxKit rows and points to warmup enable.")
|
|
1039
|
+
.option("--tenant <id>", "Deprecated and ignored: OXYGEN Warm-up consent is per mailbox, so there is no tenant-wide scope to bind. Still accepted so older scripts keep running.")
|
|
1040
|
+
.option("--approved", "Mint the per-mailbox consent links for the exact previewed mailbox scope. Still 0 credits — a human then has to open each link.")
|
|
1041
|
+
.option("--plan <hash>", "Fresh setup preview hash (required with --approved).")
|
|
1042
|
+
.option("--max-credits <n>", "Hard Oxygen credit cap; this setup requires exactly 0 (required with --approved).")
|
|
1043
|
+
.option("--status", "Poll the per-mailbox consents instead of creating a new setup preview: reports which mailboxes are authorized and which are still waiting for someone to open their link.")
|
|
1044
|
+
.option("--json", "Print a JSON envelope.")
|
|
1045
|
+
.action(async (options) => {
|
|
1046
|
+
await handleAsyncAction("mailboxes warmup microsoft", options, () => {
|
|
1047
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1048
|
+
const tenantId = readOption(options.tenant);
|
|
1049
|
+
const planHash = readOption(options.plan);
|
|
1050
|
+
const maxCredits = options.approved
|
|
1051
|
+
? readNonNegativeNumber(options.maxCredits)
|
|
1052
|
+
: undefined;
|
|
1053
|
+
if (options.status && options.approved) {
|
|
1054
|
+
throw new Error("--status cannot be combined with --approved.");
|
|
1055
|
+
}
|
|
1056
|
+
if (options.approved && !planHash) {
|
|
1057
|
+
throw new Error("--plan <hash> from the fresh Microsoft setup preview is required with --approved.");
|
|
1058
|
+
}
|
|
1059
|
+
if (options.approved && maxCredits !== 0) {
|
|
1060
|
+
throw new Error("--max-credits 0 is required with --approved; tenant setup consumes no Oxygen credits.");
|
|
1061
|
+
}
|
|
1062
|
+
return requestOxygen("/api/cli/mailboxes/warmup/microsoft", {
|
|
1063
|
+
method: "POST",
|
|
1064
|
+
body: {
|
|
1065
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
1066
|
+
...(tenantId ? { tenant_id: tenantId } : {}),
|
|
1067
|
+
...(options.status ? { status: true } : {}),
|
|
1068
|
+
...(options.approved ? { approved: true } : {}),
|
|
1069
|
+
...(planHash ? { plan_hash: planHash } : {}),
|
|
1070
|
+
...(maxCredits !== undefined
|
|
1071
|
+
? { max_credits: maxCredits }
|
|
1072
|
+
: {}),
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
}))
|
|
1077
|
+
.addCommand(new Command("config")
|
|
1078
|
+
.description("Read or override optional provider warm-up traffic, separate from the campaign ramp. Standard defaults: start 1/day, add 1/day, maximum 10/day; dedicated-tenant profiles stay stricter. No flags reads the effective plan. Override with all four knobs, or --reset for defaults. OXYGEN Warm-up does not support reply-rate control: the value is saved but not applied there. Active warm-up syncs immediately without re-enrollment or rebilling; provider_sync reports the result. 0 Oxygen credits.")
|
|
1079
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
1080
|
+
.option("--initial-per-day <n>", "Warmup emails sent on day one.")
|
|
1081
|
+
.option("--increase-per-day <n>", "Warmup emails added to the daily volume each day.")
|
|
1082
|
+
.option("--max-per-day <n>", "The warmup daily ceiling; climbing stops here.")
|
|
1083
|
+
.option("--reply-rate <percent>", "Requested warm-up reply rate (0-100); saved but unsupported by OXYGEN Warm-up.")
|
|
1084
|
+
.option("--reset", "Drop the override and restore the default provider warm-up plan.")
|
|
1085
|
+
.option("--json", "Print a JSON envelope.")
|
|
1086
|
+
.action(async (mailbox, options) => {
|
|
1087
|
+
await handleAsyncAction("mailboxes warmup config", options, () => {
|
|
1088
|
+
const path = `/api/cli/mailboxes/${encodeURIComponent(mailbox)}/warmup-config`;
|
|
1089
|
+
if (options.reset) {
|
|
1090
|
+
return requestOxygen(path, {
|
|
1091
|
+
method: "PATCH",
|
|
1092
|
+
body: { warmup_config: null },
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const knobs = {
|
|
1096
|
+
initial_per_day: readOption(options.initialPerDay),
|
|
1097
|
+
increase_per_day: readOption(options.increasePerDay),
|
|
1098
|
+
max_per_day: readOption(options.maxPerDay),
|
|
1099
|
+
reply_rate_percent: readOption(options.replyRate),
|
|
1100
|
+
};
|
|
1101
|
+
const provided = Object.values(knobs).filter((value) => value !== null && value !== undefined);
|
|
1102
|
+
// Reading is a GET on the mailbox: the derived plan is a property
|
|
1103
|
+
// of the mailbox, not of an override that may not exist.
|
|
1104
|
+
if (provided.length === 0) {
|
|
1105
|
+
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`);
|
|
1106
|
+
}
|
|
1107
|
+
if (provided.length < 4) {
|
|
1108
|
+
throw new OxygenError("invalid_request", "Overriding the ramp needs all four knobs: --initial-per-day, --increase-per-day, --max-per-day, --reply-rate. Omit them all to show the derived plan, or pass --reset to clear an override.", { exitCode: 2 });
|
|
1109
|
+
}
|
|
1110
|
+
return requestOxygen(path, {
|
|
1111
|
+
method: "PATCH",
|
|
1112
|
+
body: {
|
|
1113
|
+
warmup_config: {
|
|
1114
|
+
initial_per_day: Number(knobs.initial_per_day),
|
|
1115
|
+
increase_per_day: Number(knobs.increase_per_day),
|
|
1116
|
+
max_per_day: Number(knobs.max_per_day),
|
|
1117
|
+
reply_rate_percent: Number(knobs.reply_rate_percent),
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
}))
|
|
1123
|
+
.addCommand(new Command("profile")
|
|
1124
|
+
.description("Pin the SENDING ARCHITECTURE a domain is run as, which every inbox on it inherits: `standard` (a few mailboxes on a real business tenant, each climbing to a real number), `dedicated_tenant` (infrastructure tenant, volume from breadth, per-inbox number stays small), or `entry` (first-touch inboxes that need presence, not volume). What you state is the DOMAIN's daily budget; OXYGEN divides it by how many inboxes share that domain, so adding an inbox lowers the others instead of raising the domain's total — the thing a flat per-mailbox cap cannot do. A profile is a CEILING: it never raises a cap above what you configured, and the strictest of {configured cap, per-sequence override, sending-day ramp, profile share} still wins. 0 credits, no provider call. Without --approved this previews the before/after per domain. Omit every selector and profile to list the available profiles.")
|
|
1125
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
1126
|
+
// NOT --profile: the program declares a GLOBAL --profile (the stored
|
|
1127
|
+
// CLI credential profile), and Commander resolves the value to that
|
|
1128
|
+
// one, so the subcommand's copy silently reads empty. --type is also
|
|
1129
|
+
// the word an operator uses for this ("a specific warm-up type").
|
|
1130
|
+
.option("--type <name>", "Warm-up type to pin: standard, dedicated_tenant, or entry.")
|
|
1131
|
+
.option("--domains <list>", "Comma-separated domains to bind. Every inbox on them inherits the profile.")
|
|
1132
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to override individually — the exception path, not the bulk path.")
|
|
1133
|
+
.option("--all", "Bind every domain that currently holds a mailbox.")
|
|
1134
|
+
.option("--cold-send-per-day <n>", "Override the profile's DOMAIN budget for cold sends per day (not a per-inbox number).")
|
|
1135
|
+
.option("--warmup-per-day <n>", "Override the profile's DOMAIN budget for warm-up emails per day (not a per-inbox number).")
|
|
1136
|
+
.option("--clear", "Remove the profile, restoring inference and the sending-day ramp alone.")
|
|
1137
|
+
.option("--approved", "Apply the previewed change.")
|
|
1138
|
+
.option("--json", "Print a JSON envelope.")
|
|
1139
|
+
.action(async (options) => {
|
|
1140
|
+
await handleAsyncAction("mailboxes warmup profile", options, () => {
|
|
1141
|
+
const domains = readCsvOption(options.domains);
|
|
1142
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1143
|
+
const all = options.all === true;
|
|
1144
|
+
const clear = options.clear === true;
|
|
1145
|
+
const profile = readOption(options.type);
|
|
1146
|
+
// No selector and no profile is the discovery call: show what
|
|
1147
|
+
// the profiles ARE rather than erroring on a missing flag.
|
|
1148
|
+
if (!all && domains.length === 0 && mailboxes.length === 0 && !profile && !clear) {
|
|
1149
|
+
return requestOxygen("/api/cli/mailboxes/warmup-profile");
|
|
1150
|
+
}
|
|
1151
|
+
if (!profile && !clear) {
|
|
1152
|
+
throw new OxygenError("invalid_request", "Name a --type to pin, or pass --clear to remove one.", { exitCode: 2 });
|
|
1153
|
+
}
|
|
1154
|
+
const coldSend = readOption(options.coldSendPerDay);
|
|
1155
|
+
const warmupPerDay = readOption(options.warmupPerDay);
|
|
1156
|
+
if ((coldSend === null) !== (warmupPerDay === null)) {
|
|
1157
|
+
throw new OxygenError("invalid_request", "A budget override needs BOTH --cold-send-per-day and --warmup-per-day; they are separate budgets and defaulting one silently would misstate the domain's ceiling.", { exitCode: 2 });
|
|
1158
|
+
}
|
|
1159
|
+
return requestOxygen("/api/cli/mailboxes/warmup-profile", {
|
|
1160
|
+
method: "POST",
|
|
1161
|
+
body: {
|
|
1162
|
+
profile: clear ? null : profile,
|
|
1163
|
+
...(coldSend !== null && warmupPerDay !== null
|
|
1164
|
+
? {
|
|
1165
|
+
budget: {
|
|
1166
|
+
cold_send_per_day: Number(coldSend),
|
|
1167
|
+
warmup_per_day: Number(warmupPerDay),
|
|
1168
|
+
},
|
|
1169
|
+
}
|
|
1170
|
+
: {}),
|
|
1171
|
+
...(domains.length > 0 ? { domains } : {}),
|
|
1172
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
1173
|
+
...(all ? { all: true } : {}),
|
|
1174
|
+
...(options.approved === true ? { approved: true } : {}),
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
}))
|
|
1179
|
+
.addCommand(new Command("pause")
|
|
1180
|
+
.description("Pause warmup at the rail that actually enrolled each mailbox — OXYGEN Warm-up for current enrollments, TrulyInbox for inboxes still warming there. A mailbox left on a rail Oxygen no longer drives reports unsupported instead of being retargeted, so the reply never claims a pause that did not happen. Targets the whole pool unless --mailboxes is given.")
|
|
1181
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
1182
|
+
.option("--json", "Print a JSON envelope.")
|
|
1183
|
+
.action(async (options) => {
|
|
1184
|
+
await handleAsyncAction("mailboxes warmup pause", options, () => {
|
|
1185
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1186
|
+
return requestOxygen("/api/cli/mailboxes/warmup/pause", {
|
|
1187
|
+
method: "POST",
|
|
1188
|
+
body: { ...(mailboxes.length > 0 ? { mailboxes } : {}) },
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
}))
|
|
1192
|
+
.addCommand(new Command("resume")
|
|
1193
|
+
.description("Resume paused warmup at each mailbox's recorded rail (OXYGEN Warm-up, or TrulyInbox for inboxes still warming there). 0 Oxygen credits; the existing subscription is unchanged. This changes warmup state and uses the last stored analytics snapshot; run warmup status afterward for fresh provider data. Running status alone does not prove a new successful send. A mailbox on a rail Oxygen no longer drives reports unsupported instead of being retargeted. Targets the whole pool unless --mailboxes is given.")
|
|
1194
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
1195
|
+
.option("--json", "Print a JSON envelope.")
|
|
1196
|
+
.action(async (options) => {
|
|
1197
|
+
await handleAsyncAction("mailboxes warmup resume", options, () => {
|
|
1198
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1199
|
+
return requestOxygen("/api/cli/mailboxes/warmup/resume", {
|
|
1200
|
+
method: "POST",
|
|
1201
|
+
body: { ...(mailboxes.length > 0 ? { mailboxes } : {}) },
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
}))
|
|
1205
|
+
.addCommand(new Command("reconnect")
|
|
1206
|
+
.description("Manual repair for an existing live paid seat when automatic credential repair is disabled, exhausted, or needs a new credential: push the mailbox's CURRENT credential to its warmup provider. Use this when a new app password is needed because the old one was rotated or revoked (the provider reporting 'Username and Password not accepted'): re-import the new credential with `mailboxes import`, then run this. It costs 0 credits and leaves your warmup subscription and its renewal date exactly as they are — you do NOT need to disable and re-enable warmup, which would charge a second month. The provider cannot change a stored password in place, so the inbox's warmup history restarts from day 1. Targets the whole pool unless --mailboxes is given.")
|
|
1207
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
1208
|
+
.option("--json", "Print a JSON envelope.")
|
|
1209
|
+
.action(async (options) => {
|
|
1210
|
+
await handleAsyncAction("mailboxes warmup reconnect", options, () => {
|
|
1211
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1212
|
+
return requestOxygen("/api/cli/mailboxes/warmup/reconnect", {
|
|
1213
|
+
method: "POST",
|
|
1214
|
+
body: {
|
|
1215
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
}))
|
|
1220
|
+
.addCommand(new Command("reactivate")
|
|
1221
|
+
.description("Manual reactivation of an existing live paid seat: ask the warm-up provider to re-run the connection check on an inbox it has DISABLED, without touching the credential, subscription, or ramp day. When worker recovery is enforcing, OXYGEN first tries this vendor reactivation automatically for a Microsoft OAuth or tenant-consent seat, without a password or new browser consent. Use this command while recovery is staged in shadow/off, or if `warmup status` still asks you to reactivate after bounded attempts; `warmup reconnect` cannot re-push a password for that consent seat. 0 credits: the subscription, its renewal date, and the ramp day all stay exactly as they are, so it is the free alternative to disable + re-enable (which bills another warm-up month AND restarts the ramp at day 1). Only inboxes with a live warm-up subscription are eligible. IMPORTANT: this forces the provider to re-check; a forced pass is NOT proof the connection works. Confirm recovery from a rising send count with `oxygen mailboxes warmup status` after the provider's next batch, which is hours away, not minutes. Targets the whole pool unless --mailboxes is given.")
|
|
1222
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
1223
|
+
.option("--json", "Print a JSON envelope.")
|
|
1224
|
+
.action(async (options) => {
|
|
1225
|
+
await handleAsyncAction("mailboxes warmup reactivate", options, () => {
|
|
1226
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1227
|
+
return requestOxygen("/api/cli/mailboxes/warmup/reactivate", {
|
|
1228
|
+
method: "POST",
|
|
1229
|
+
body: {
|
|
1230
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
}))
|
|
1235
|
+
.addCommand(new Command("disable")
|
|
1236
|
+
.description("Disable warmup and unenroll each mailbox from the rail that actually enrolled it. On a managed rail — OXYGEN Warm-up today, TrulyInbox for older enrollments — this also cancels that mailbox's warmup subscription, so billing stops with the provider removal. This is how you wind an inbox off the retired TrulyInbox rail. NOT the way to fix a rotated app password — use `mailboxes warmup reconnect` for that, which costs 0 credits instead of a new 100-credit month. Targets the whole pool unless --mailboxes is given.")
|
|
1237
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
1238
|
+
.option("--json", "Print a JSON envelope.")
|
|
1239
|
+
.action(async (options) => {
|
|
1240
|
+
await handleAsyncAction("mailboxes warmup disable", options, () => {
|
|
1241
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1242
|
+
return requestOxygen("/api/cli/mailboxes/warmup/disable", {
|
|
1243
|
+
method: "POST",
|
|
1244
|
+
body: { ...(mailboxes.length > 0 ? { mailboxes } : {}) },
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
}))
|
|
1248
|
+
.addCommand(new Command("status")
|
|
1249
|
+
.description("Read warmup analytics back from the rail each mailbox is enrolled on and refresh each mailbox's warmupTruth (state, last send, pause reason, dispatch counters, next_action). A read: 0 Oxygen credits, no campaign sends, no change to your configuration; the one write OXYGEN may make is its own breaker pausing a mailbox proven to be dispatching far above its ramp. Targets the whole pool unless --mailboxes is given. For a per-mailbox read without a rail sync, use `oxygen mailboxes get <address> --json`.")
|
|
1250
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to sync. Omit to sync the whole pool.")
|
|
1251
|
+
.option("--provider <name>", "Optional machine routing filter. Omit it for OXYGEN Warm-up; pass trulyinbox only to read inboxes still warming on the retired rail.")
|
|
1252
|
+
.option("--dry-run", "Skip the provider call (mailboxes marked pending).")
|
|
1253
|
+
.option("--json", "Print a JSON envelope.")
|
|
1254
|
+
.action(async (options) => {
|
|
1255
|
+
await handleAsyncAction("mailboxes warmup status", options, () => {
|
|
1256
|
+
const params = new URLSearchParams();
|
|
1257
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
1258
|
+
if (mailboxes.length > 0)
|
|
1259
|
+
params.set("mailboxes", mailboxes.join(","));
|
|
1260
|
+
const connection = readOption(options.connection);
|
|
1261
|
+
if (connection)
|
|
1262
|
+
params.set("connection_id", connection);
|
|
1263
|
+
const billingMode = readOption(options.billingMode);
|
|
1264
|
+
if (billingMode)
|
|
1265
|
+
params.set("billing_mode", billingMode);
|
|
1266
|
+
const provider = readOption(options.provider);
|
|
1267
|
+
if (provider)
|
|
1268
|
+
params.set("provider", provider);
|
|
1269
|
+
if (options.dryRun)
|
|
1270
|
+
params.set("dry_run", "true");
|
|
1271
|
+
const suffix = params.toString();
|
|
1272
|
+
return requestOxygen(`/api/cli/mailboxes/warmup/status${suffix ? `?${suffix}` : ""}`);
|
|
1273
|
+
});
|
|
1274
|
+
})))
|
|
1275
|
+
.addCommand(new Command("order")
|
|
1276
|
+
.description("RETIRED — managed mailboxes are now purchased as monthly subscriptions via `oxygen managed-inboxes subscribe` (InboxKit-backed). This legacy Zapmail order path was never enabled anywhere and now always fails with code `gone`.")
|
|
1277
|
+
// Swallow the old flags (--provider/--domains/--approved/...) so a
|
|
1278
|
+
// stale invocation reaches the tombstone envelope instead of dying
|
|
1279
|
+
// on commander's unknown-option parse error.
|
|
1280
|
+
.allowUnknownOption(true)
|
|
1281
|
+
.allowExcessArguments(true)
|
|
1282
|
+
.action(() => {
|
|
1283
|
+
emitCliFailure("mailboxes order", new OxygenError("gone", "`oxygen mailboxes order` is retired — managed mailboxes are now purchased as monthly subscriptions via `oxygen managed-inboxes subscribe` (InboxKit-backed). Preview a quote, then re-run with --approved --quote <id>. Nothing was ordered or charged.", {
|
|
1284
|
+
details: {
|
|
1285
|
+
replacement: "oxygen managed-inboxes subscribe",
|
|
1286
|
+
deep_link: "https://oxygen-agent.com/billing",
|
|
1287
|
+
},
|
|
1288
|
+
}));
|
|
1289
|
+
})));
|
|
1290
|
+
}
|