@oxygen-agent/cli 1.662.0 → 1.681.2
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/index.js +101 -439
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +6 -1
- package/node_modules/@oxygen/shared/dist/collab.d.ts +6 -5
- package/node_modules/@oxygen/shared/dist/collab.js +11 -4
- package/node_modules/@oxygen/shared/dist/credit-guidance.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +90 -1
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +16 -13
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +556 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/tags.js +7 -1
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +5 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,8 @@ import { applyOxygenHelp } from "./help.js";
|
|
|
11
11
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
12
12
|
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
13
13
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
14
|
-
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
14
|
+
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
15
|
+
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";
|
|
15
16
|
import { assertRecipeBundleSafe, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, } from "@oxygen/workflows";
|
|
16
17
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
17
18
|
import { createBrowserLoginSession, createRelayLoginSession, openBrowser } from "./browser-login.js";
|
|
@@ -894,413 +895,26 @@ function readJsonFileValue(path, inputName) {
|
|
|
894
895
|
* shortcut) so the server applies the encrypted transfer-vault contract.
|
|
895
896
|
*/
|
|
896
897
|
function normalizeMailboxImportFile(value, mode) {
|
|
897
|
-
|
|
898
|
-
|
|
898
|
+
try {
|
|
899
|
+
return normalizeSharedMailboxImportFile(value, mode);
|
|
899
900
|
}
|
|
900
|
-
|
|
901
|
-
if (
|
|
902
|
-
|
|
901
|
+
catch (error) {
|
|
902
|
+
if (error instanceof OxygenError &&
|
|
903
|
+
error.message ===
|
|
904
|
+
"This identity-only import contains a password. Choose a secure credential import for a compatible Google app-password export.") {
|
|
905
|
+
throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source> (or --from hypertide for a Hypertide export); keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
|
|
903
906
|
}
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
return dedupeMailboxImportRows(rows);
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
907
909
|
}
|
|
908
910
|
function summarizeMailboxImportValidation(mailboxes, input) {
|
|
909
|
-
const providers = { google: 0, microsoft: 0 };
|
|
910
|
-
const infrastructurePlatforms = {};
|
|
911
|
-
let credentialRows = 0;
|
|
912
|
-
for (const mailbox of mailboxes) {
|
|
913
|
-
if (mailbox.provider === "google")
|
|
914
|
-
providers.google += 1;
|
|
915
|
-
if (mailbox.provider === "microsoft")
|
|
916
|
-
providers.microsoft += 1;
|
|
917
|
-
if (typeof mailbox.infrastructure_platform === "string") {
|
|
918
|
-
const platform = mailbox.infrastructure_platform;
|
|
919
|
-
infrastructurePlatforms[platform] =
|
|
920
|
-
(infrastructurePlatforms[platform] ?? 0) + 1;
|
|
921
|
-
}
|
|
922
|
-
if (typeof mailbox.app_password === "string")
|
|
923
|
-
credentialRows += 1;
|
|
924
|
-
}
|
|
925
911
|
return {
|
|
926
|
-
|
|
927
|
-
source: input.source,
|
|
928
|
-
...(input.sourceProvider
|
|
929
|
-
? { source_provider: input.sourceProvider }
|
|
930
|
-
: {}),
|
|
931
|
-
rows: mailboxes.length,
|
|
932
|
-
providers,
|
|
933
|
-
infrastructure_platforms: infrastructurePlatforms,
|
|
934
|
-
credential_rows: credentialRows,
|
|
935
|
-
identity_only_rows: mailboxes.length - credentialRows,
|
|
936
|
-
limits: {
|
|
937
|
-
max_rows: MAILBOX_IMPORT_ROW_LIMIT,
|
|
938
|
-
max_file_bytes: MAILBOX_IMPORT_FILE_MAX_BYTES,
|
|
939
|
-
},
|
|
940
|
-
mutation: false,
|
|
941
|
-
network_request: false,
|
|
942
|
-
provider_call: false,
|
|
943
|
-
credits_used: 0,
|
|
912
|
+
...summarizeSharedMailboxImportValidation(mailboxes, input),
|
|
944
913
|
next_action: "Re-run the same command without --validate-only to register these mailboxes.",
|
|
945
914
|
};
|
|
946
915
|
}
|
|
947
|
-
const MAILBOX_EMAIL_HEADERS = new Set([
|
|
948
|
-
"address",
|
|
949
|
-
"email",
|
|
950
|
-
"emailaddress",
|
|
951
|
-
"fromemail",
|
|
952
|
-
"mailbox",
|
|
953
|
-
"mailboxaddress",
|
|
954
|
-
"mailboxemail",
|
|
955
|
-
]);
|
|
956
|
-
const MAILBOX_PROVIDER_HEADERS = new Set([
|
|
957
|
-
"emailprovider",
|
|
958
|
-
"esp",
|
|
959
|
-
"mailboxprovider",
|
|
960
|
-
"platform",
|
|
961
|
-
"provider",
|
|
962
|
-
"serviceprovider",
|
|
963
|
-
"type",
|
|
964
|
-
]);
|
|
965
|
-
const MAILBOX_PASSWORD_HEADERS = new Set([
|
|
966
|
-
"apppassword",
|
|
967
|
-
"applicationpassword",
|
|
968
|
-
"googleapppassword",
|
|
969
|
-
"imappassword",
|
|
970
|
-
"mailboxpassword",
|
|
971
|
-
"password",
|
|
972
|
-
"smtppassword",
|
|
973
|
-
]);
|
|
974
|
-
const MAILBOX_WORKSPACE_HEADERS = new Set([
|
|
975
|
-
"externalaccountid",
|
|
976
|
-
"mailboxid",
|
|
977
|
-
"mailboxuid",
|
|
978
|
-
"sourceaccountid",
|
|
979
|
-
"sourcemailboxid",
|
|
980
|
-
"uid",
|
|
981
|
-
"workspaceexternalid",
|
|
982
|
-
]);
|
|
983
|
-
const MAILBOX_PLATFORM_HEADERS = new Set([
|
|
984
|
-
"infrastructure",
|
|
985
|
-
"infrastructureplatform",
|
|
986
|
-
"infraplatform",
|
|
987
|
-
]);
|
|
988
|
-
const MAILBOX_TENANT_HEADERS = new Set([
|
|
989
|
-
"azuretenantid",
|
|
990
|
-
"entratenantid",
|
|
991
|
-
"microsofttenantid",
|
|
992
|
-
"tenantid",
|
|
993
|
-
]);
|
|
994
|
-
const MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS = new Set([
|
|
995
|
-
"accesstoken",
|
|
996
|
-
"applicationsecret",
|
|
997
|
-
"authenticatorsecret",
|
|
998
|
-
"authorization",
|
|
999
|
-
"authorizationcode",
|
|
1000
|
-
"bearertoken",
|
|
1001
|
-
"clientsecret",
|
|
1002
|
-
"clientprivatekey",
|
|
1003
|
-
"delegationkey",
|
|
1004
|
-
"idtoken",
|
|
1005
|
-
"mfacode",
|
|
1006
|
-
"mfasecret",
|
|
1007
|
-
"oauthaccesstoken",
|
|
1008
|
-
"oauthcode",
|
|
1009
|
-
"oauthrefreshtoken",
|
|
1010
|
-
"oauthtoken",
|
|
1011
|
-
"onetimepassword",
|
|
1012
|
-
"otp",
|
|
1013
|
-
"otpsecret",
|
|
1014
|
-
"privatekey",
|
|
1015
|
-
"refreshtoken",
|
|
1016
|
-
"serviceaccountkey",
|
|
1017
|
-
"serviceaccountjson",
|
|
1018
|
-
"serviceaccountprivatekey",
|
|
1019
|
-
"totp",
|
|
1020
|
-
"totpsecret",
|
|
1021
|
-
]);
|
|
1022
|
-
const MAILBOX_IMPORT_FILE_MAX_BYTES = 5 * 1024 * 1024;
|
|
1023
|
-
const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
1024
|
-
/**
|
|
1025
|
-
* Normalize common mailbox-vendor export labels locally, then send only
|
|
1026
|
-
* Oxygen's canonical fields. Passwords are accepted only in credential mode
|
|
1027
|
-
* and only become Google app passwords; Microsoft passwords are dropped because
|
|
1028
|
-
* one Outlook OAuth consent per mailbox is its only warmup path. OAuth grants,
|
|
1029
|
-
* MFA/TOTP seeds, and delegation keys are rejected in every mode. Host columns
|
|
1030
|
-
* may prove the provider but never cross the request boundary.
|
|
1031
|
-
*/
|
|
1032
|
-
function normalizeMailboxExportRow(row, index, mode) {
|
|
1033
|
-
const byHeader = new Map();
|
|
1034
|
-
for (const [header, value] of Object.entries(row)) {
|
|
1035
|
-
const normalizedHeader = normalizeMailboxExportHeader(header);
|
|
1036
|
-
const existing = byHeader.get(normalizedHeader);
|
|
1037
|
-
if (existing !== undefined &&
|
|
1038
|
-
String(existing).trim() !== String(value ?? "").trim()) {
|
|
1039
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting columns that normalize to ${normalizedHeader}.`, { exitCode: 1 });
|
|
1040
|
-
}
|
|
1041
|
-
byHeader.set(normalizedHeader, value);
|
|
1042
|
-
}
|
|
1043
|
-
const email = readUniqueMailboxExportString(byHeader, MAILBOX_EMAIL_HEADERS, index, "email_address", (value) => value.trim().toLowerCase()) ?? readEmailShapedMailboxUsername(byHeader);
|
|
1044
|
-
const providerValues = readMailboxExportStrings(byHeader, MAILBOX_PROVIDER_HEADERS);
|
|
1045
|
-
const provider = normalizeMailboxProvider(providerValues, byHeader, index);
|
|
1046
|
-
if (!email ||
|
|
1047
|
-
email.length > 320 ||
|
|
1048
|
-
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1049
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].email_address must be a valid address of at most 320 characters.`, { exitCode: 1 });
|
|
1050
|
-
}
|
|
1051
|
-
if (!provider) {
|
|
1052
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].provider could not be inferred. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credentials are not transferable yet.`, { exitCode: 1 });
|
|
1053
|
-
}
|
|
1054
|
-
if (hasMailboxExportValue(byHeader, MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS) ||
|
|
1055
|
-
hasNonTransferableMailboxAuthLikeValue(byHeader)) {
|
|
1056
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains OAuth, MFA/TOTP, client-secret, or delegation material. Those credentials are never transferable; import the mailbox identity and complete the provider's administrator consent flow.`, { exitCode: 1 });
|
|
1057
|
-
}
|
|
1058
|
-
const passwords = readMailboxExportStrings(byHeader, MAILBOX_PASSWORD_HEADERS);
|
|
1059
|
-
const distinctPasswords = [...new Set(passwords)];
|
|
1060
|
-
if (distinctPasswords.some((password) => password.length > 1024)) {
|
|
1061
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].app_password must be at most 1024 characters.`, { exitCode: 1 });
|
|
1062
|
-
}
|
|
1063
|
-
if (mode === "identity" && distinctPasswords.length > 0) {
|
|
1064
|
-
throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source> (or --from hypertide for a Hypertide export); keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
|
|
1065
|
-
}
|
|
1066
|
-
if (provider !== "microsoft" && distinctPasswords.length > 1) {
|
|
1067
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting SMTP/IMAP/app-password values.`, { exitCode: 1 });
|
|
1068
|
-
}
|
|
1069
|
-
if (mode === "credential" &&
|
|
1070
|
-
provider === "google" &&
|
|
1071
|
-
distinctPasswords.length === 0) {
|
|
1072
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] needs a Google app password for credential import. OAuth tokens and an ordinary account password are not portable.`, { exitCode: 1 });
|
|
1073
|
-
}
|
|
1074
|
-
const workspaceExternalId = readUniqueMailboxExportString(byHeader, MAILBOX_WORKSPACE_HEADERS, index, "workspace_external_id", (value) => value.trim());
|
|
1075
|
-
if (workspaceExternalId && workspaceExternalId.length > 512) {
|
|
1076
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].workspace_external_id must be at most 512 characters.`, { exitCode: 1 });
|
|
1077
|
-
}
|
|
1078
|
-
const tenantId = readUniqueMailboxExportString(byHeader, MAILBOX_TENANT_HEADERS, index, "tenant_id", (value) => value.trim().toLowerCase());
|
|
1079
|
-
if (tenantId && provider !== "microsoft") {
|
|
1080
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id applies only to Microsoft mailboxes.`, { exitCode: 1 });
|
|
1081
|
-
}
|
|
1082
|
-
if (tenantId &&
|
|
1083
|
-
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tenantId.trim())) {
|
|
1084
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id must be a Microsoft Entra tenant GUID.`, { exitCode: 1 });
|
|
1085
|
-
}
|
|
1086
|
-
const infrastructurePlatformRaw = readUniqueMailboxExportString(byHeader, MAILBOX_PLATFORM_HEADERS, index, "infrastructure_platform", (value) => normalizeMailboxExportHeader(value)) ??
|
|
1087
|
-
(providerValues.some((value) => ["azure", "entra", "microsoftazure"].includes(normalizeMailboxExportHeader(value)))
|
|
1088
|
-
? "microsoft_azure"
|
|
1089
|
-
: null);
|
|
1090
|
-
const infrastructurePlatform = normalizeMailboxInfrastructurePlatform(infrastructurePlatformRaw, provider, index);
|
|
1091
|
-
return {
|
|
1092
|
-
email_address: email.trim().toLowerCase(),
|
|
1093
|
-
provider,
|
|
1094
|
-
...(workspaceExternalId !== null
|
|
1095
|
-
? { workspace_external_id: workspaceExternalId.trim() }
|
|
1096
|
-
: {}),
|
|
1097
|
-
...(infrastructurePlatformRaw
|
|
1098
|
-
? { infrastructure_platform: infrastructurePlatform }
|
|
1099
|
-
: {}),
|
|
1100
|
-
...(tenantId ? { tenant_id: tenantId.trim().toLowerCase() } : {}),
|
|
1101
|
-
...(mode === "credential" &&
|
|
1102
|
-
provider === "google" &&
|
|
1103
|
-
distinctPasswords[0] !== undefined
|
|
1104
|
-
? { app_password: distinctPasswords[0] }
|
|
1105
|
-
: {}),
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
function normalizeMailboxExportHeader(value) {
|
|
1109
|
-
return value
|
|
1110
|
-
.replace(/^\uFEFF/, "")
|
|
1111
|
-
.normalize("NFKC")
|
|
1112
|
-
.trim()
|
|
1113
|
-
.toLowerCase()
|
|
1114
|
-
.replace(/[^a-z0-9]/g, "");
|
|
1115
|
-
}
|
|
1116
|
-
function readMailboxExportString(byHeader, headers) {
|
|
1117
|
-
for (const header of headers) {
|
|
1118
|
-
const value = byHeader.get(header);
|
|
1119
|
-
if (typeof value === "string" && value.length > 0)
|
|
1120
|
-
return value;
|
|
1121
|
-
}
|
|
1122
|
-
return null;
|
|
1123
|
-
}
|
|
1124
|
-
function readUniqueMailboxExportString(byHeader, headers, index, field, normalize) {
|
|
1125
|
-
const values = readMailboxExportStrings(byHeader, headers);
|
|
1126
|
-
if (values.length === 0)
|
|
1127
|
-
return null;
|
|
1128
|
-
const normalized = [...new Set(values.map(normalize))];
|
|
1129
|
-
if (normalized.length > 1) {
|
|
1130
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting ${field} aliases.`, { exitCode: 1 });
|
|
1131
|
-
}
|
|
1132
|
-
return normalized[0] ?? null;
|
|
1133
|
-
}
|
|
1134
|
-
function readMailboxExportStrings(byHeader, headers) {
|
|
1135
|
-
const values = [];
|
|
1136
|
-
for (const header of headers) {
|
|
1137
|
-
const value = byHeader.get(header);
|
|
1138
|
-
if (typeof value === "string" && value.length > 0)
|
|
1139
|
-
values.push(value);
|
|
1140
|
-
}
|
|
1141
|
-
return values;
|
|
1142
|
-
}
|
|
1143
|
-
function hasMailboxExportValue(byHeader, headers) {
|
|
1144
|
-
for (const header of headers) {
|
|
1145
|
-
const value = byHeader.get(header);
|
|
1146
|
-
if (value !== undefined && value !== null && String(value).trim()) {
|
|
1147
|
-
return true;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
return false;
|
|
1151
|
-
}
|
|
1152
|
-
function hasNonTransferableMailboxAuthLikeValue(byHeader) {
|
|
1153
|
-
for (const [header, value] of byHeader) {
|
|
1154
|
-
if (/(?:token|secret|privatekey|totp|passcode|authorization)/.test(header) &&
|
|
1155
|
-
value !== undefined &&
|
|
1156
|
-
value !== null &&
|
|
1157
|
-
String(value).trim()) {
|
|
1158
|
-
return true;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
return false;
|
|
1162
|
-
}
|
|
1163
|
-
function readEmailShapedMailboxUsername(byHeader) {
|
|
1164
|
-
for (const header of ["username", "imapusername", "smtpusername"]) {
|
|
1165
|
-
const value = byHeader.get(header);
|
|
1166
|
-
if (typeof value === "string" && value.includes("@"))
|
|
1167
|
-
return value.trim().toLowerCase();
|
|
1168
|
-
}
|
|
1169
|
-
return null;
|
|
1170
|
-
}
|
|
1171
|
-
function normalizeMailboxProvider(providerValues, byHeader, index) {
|
|
1172
|
-
const explicit = providerValues.map((value) => {
|
|
1173
|
-
const normalized = normalizeMailboxExportHeader(value);
|
|
1174
|
-
if (["google", "googleworkspace", "gmail", "gsuite"].includes(normalized)) {
|
|
1175
|
-
return "google";
|
|
1176
|
-
}
|
|
1177
|
-
if ([
|
|
1178
|
-
"azure",
|
|
1179
|
-
"entra",
|
|
1180
|
-
"m365",
|
|
1181
|
-
"microsoft",
|
|
1182
|
-
"microsoft365",
|
|
1183
|
-
"microsoftazure",
|
|
1184
|
-
"ms365",
|
|
1185
|
-
"o365",
|
|
1186
|
-
"office365",
|
|
1187
|
-
"outlook",
|
|
1188
|
-
].includes(normalized)) {
|
|
1189
|
-
return "microsoft";
|
|
1190
|
-
}
|
|
1191
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].provider is unsupported; expected Google/Gmail/Workspace or Microsoft/Outlook/Office 365/Entra.`, { exitCode: 1 });
|
|
1192
|
-
});
|
|
1193
|
-
const explicitKinds = [...new Set(explicit)];
|
|
1194
|
-
if (explicitKinds.length > 1) {
|
|
1195
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting provider aliases.`, { exitCode: 1 });
|
|
1196
|
-
}
|
|
1197
|
-
const hostValues = [
|
|
1198
|
-
byHeader.get("imaphost"),
|
|
1199
|
-
byHeader.get("imaphostname"),
|
|
1200
|
-
byHeader.get("imapserver"),
|
|
1201
|
-
byHeader.get("smtphost"),
|
|
1202
|
-
byHeader.get("smtphostname"),
|
|
1203
|
-
byHeader.get("smtpserver"),
|
|
1204
|
-
].filter((value) => typeof value === "string" && Boolean(value.trim()));
|
|
1205
|
-
const hostKinds = hostValues.map((value) => {
|
|
1206
|
-
const hostname = normalizeMailboxHost(value);
|
|
1207
|
-
if (hostname === "smtp.gmail.com" || hostname === "imap.gmail.com") {
|
|
1208
|
-
return "google";
|
|
1209
|
-
}
|
|
1210
|
-
if (hostname === "smtp.office365.com" ||
|
|
1211
|
-
hostname === "outlook.office365.com") {
|
|
1212
|
-
return "microsoft";
|
|
1213
|
-
}
|
|
1214
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains a non-standard SMTP/IMAP host. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credential transport is not supported.`, { exitCode: 1 });
|
|
1215
|
-
});
|
|
1216
|
-
const inferredKinds = [...new Set(hostKinds)];
|
|
1217
|
-
if (inferredKinds.length > 1) {
|
|
1218
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting Google and Microsoft mail hosts.`, { exitCode: 1 });
|
|
1219
|
-
}
|
|
1220
|
-
const explicitKind = explicitKinds[0];
|
|
1221
|
-
const inferredKind = inferredKinds[0];
|
|
1222
|
-
if (explicitKind && inferredKind && explicitKind !== inferredKind) {
|
|
1223
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] has conflicting provider and SMTP/IMAP host evidence.`, { exitCode: 1 });
|
|
1224
|
-
}
|
|
1225
|
-
return explicitKind ?? inferredKind ?? null;
|
|
1226
|
-
}
|
|
1227
|
-
function normalizeMailboxHost(value) {
|
|
1228
|
-
const withoutScheme = value
|
|
1229
|
-
.trim()
|
|
1230
|
-
.toLowerCase()
|
|
1231
|
-
.replace(/^[a-z]+:\/\//, "");
|
|
1232
|
-
return withoutScheme.split(/[/:]/, 1)[0] ?? "";
|
|
1233
|
-
}
|
|
1234
|
-
function normalizeMailboxInfrastructurePlatform(raw, provider, index) {
|
|
1235
|
-
if (!raw) {
|
|
1236
|
-
return provider === "google" ? "google_workspace" : "microsoft_365";
|
|
1237
|
-
}
|
|
1238
|
-
const normalized = normalizeMailboxExportHeader(raw);
|
|
1239
|
-
if (["google", "googleworkspace", "gmail", "gsuite", "workspace"].includes(normalized)) {
|
|
1240
|
-
if (provider !== "google") {
|
|
1241
|
-
throw mailboxPlatformConflict(index, provider);
|
|
1242
|
-
}
|
|
1243
|
-
return "google_workspace";
|
|
1244
|
-
}
|
|
1245
|
-
if (["azure", "entra", "microsoftazure"].includes(normalized)) {
|
|
1246
|
-
if (provider !== "microsoft") {
|
|
1247
|
-
throw mailboxPlatformConflict(index, provider);
|
|
1248
|
-
}
|
|
1249
|
-
return "microsoft_azure";
|
|
1250
|
-
}
|
|
1251
|
-
if ([
|
|
1252
|
-
"m365",
|
|
1253
|
-
"microsoft",
|
|
1254
|
-
"microsoft365",
|
|
1255
|
-
"ms365",
|
|
1256
|
-
"o365",
|
|
1257
|
-
"office365",
|
|
1258
|
-
"outlook",
|
|
1259
|
-
].includes(normalized)) {
|
|
1260
|
-
if (provider !== "microsoft") {
|
|
1261
|
-
throw mailboxPlatformConflict(index, provider);
|
|
1262
|
-
}
|
|
1263
|
-
return "microsoft_365";
|
|
1264
|
-
}
|
|
1265
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform is unsupported; expected Google Workspace, Microsoft 365, Azure, or Entra.`, { exitCode: 1 });
|
|
1266
|
-
}
|
|
1267
|
-
function mailboxPlatformConflict(index, provider) {
|
|
1268
|
-
return new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform conflicts with provider=${provider}.`, { exitCode: 1 });
|
|
1269
|
-
}
|
|
1270
|
-
function dedupeMailboxImportRows(rows) {
|
|
1271
|
-
const unique = new Map();
|
|
1272
|
-
for (const row of rows) {
|
|
1273
|
-
const address = String(row.email_address).toLowerCase();
|
|
1274
|
-
const existing = unique.get(address);
|
|
1275
|
-
if (!existing) {
|
|
1276
|
-
unique.set(address, row);
|
|
1277
|
-
continue;
|
|
1278
|
-
}
|
|
1279
|
-
if (JSON.stringify(existing) !== JSON.stringify(row)) {
|
|
1280
|
-
throw new OxygenError("invalid_request", `Duplicate mailbox ${address} has conflicting provider, platform, tenant, external id, or credential fields.`, { exitCode: 1 });
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
return [...unique.values()];
|
|
1284
|
-
}
|
|
1285
916
|
function normalizeMailboxImportVendor(raw, from) {
|
|
1286
|
-
|
|
1287
|
-
if (raw && raw.trim().toLowerCase() !== "hypertide") {
|
|
1288
|
-
throw new OxygenError("invalid_request", "--from hypertide is fixed to --vendor hypertide.", { exitCode: 1 });
|
|
1289
|
-
}
|
|
1290
|
-
return null;
|
|
1291
|
-
}
|
|
1292
|
-
if (from === "credentials" && !raw) {
|
|
1293
|
-
throw new OxygenError("invalid_request", "--vendor <source> is required with --from credentials so the imported mailbox keeps its provenance.", { exitCode: 1 });
|
|
1294
|
-
}
|
|
1295
|
-
if (!raw)
|
|
1296
|
-
return null;
|
|
1297
|
-
const normalized = raw.trim().toLowerCase();
|
|
1298
|
-
if (normalized.length < 1 ||
|
|
1299
|
-
normalized.length > 64 ||
|
|
1300
|
-
!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(normalized)) {
|
|
1301
|
-
throw new OxygenError("invalid_request", "--vendor must be 1-64 lowercase letters, digits, dots, underscores, or hyphens.", { exitCode: 1 });
|
|
1302
|
-
}
|
|
1303
|
-
return normalized;
|
|
917
|
+
return normalizeSharedMailboxImportVendor(raw, from);
|
|
1304
918
|
}
|
|
1305
919
|
async function readMailboxImportFile(path) {
|
|
1306
920
|
let buffer;
|
|
@@ -1310,42 +924,24 @@ async function readMailboxImportFile(path) {
|
|
|
1310
924
|
catch {
|
|
1311
925
|
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 (or --from hypertide). See https://oxygen-agent.com/docs/providers/mailbox-compatibility.`, { exitCode: 1 });
|
|
1312
926
|
}
|
|
1313
|
-
if (buffer.byteLength >
|
|
1314
|
-
throw new OxygenError("invalid_request", `Mailbox import files must be ${
|
|
927
|
+
if (buffer.byteLength > SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES) {
|
|
928
|
+
throw new OxygenError("invalid_request", `Mailbox import files must be ${SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES / 1024 / 1024} MB or smaller.`, { exitCode: 1 });
|
|
1315
929
|
}
|
|
1316
930
|
const format = inferRowsFileFormat(path);
|
|
1317
931
|
let rows;
|
|
1318
|
-
if (format === "
|
|
1319
|
-
let parsed;
|
|
932
|
+
if (format === "xlsx") {
|
|
1320
933
|
try {
|
|
1321
|
-
|
|
934
|
+
rows = normalizeMailboxWorkbookRows(await parseXlsxWorkbookBuffer(buffer));
|
|
1322
935
|
}
|
|
1323
936
|
catch {
|
|
1324
|
-
throw new OxygenError("
|
|
1325
|
-
}
|
|
1326
|
-
if (Array.isArray(parsed)) {
|
|
1327
|
-
rows = parsed;
|
|
1328
|
-
}
|
|
1329
|
-
else if (parsed &&
|
|
1330
|
-
typeof parsed === "object" &&
|
|
1331
|
-
!Array.isArray(parsed) &&
|
|
1332
|
-
Array.isArray(parsed.mailboxes)) {
|
|
1333
|
-
rows = parsed.mailboxes;
|
|
1334
|
-
}
|
|
1335
|
-
else {
|
|
1336
|
-
throw new OxygenError("invalid_request", "--file JSON must contain a mailbox array or { mailboxes: [...] }.", { exitCode: 1 });
|
|
937
|
+
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 });
|
|
1337
938
|
}
|
|
1338
939
|
}
|
|
1339
940
|
else {
|
|
1340
|
-
|
|
1341
|
-
rows = await parseRowsFileBuffer(buffer, format);
|
|
1342
|
-
}
|
|
1343
|
-
catch {
|
|
1344
|
-
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 });
|
|
1345
|
-
}
|
|
941
|
+
rows = parseMailboxImportText(buffer.toString("utf8"), format);
|
|
1346
942
|
}
|
|
1347
|
-
if (rows.length >
|
|
1348
|
-
throw new OxygenError("invalid_request", `Mailbox imports are limited to ${
|
|
943
|
+
if (rows.length > SHARED_MAILBOX_IMPORT_ROW_LIMIT) {
|
|
944
|
+
throw new OxygenError("invalid_request", `Mailbox imports are limited to ${SHARED_MAILBOX_IMPORT_ROW_LIMIT} rows per request.`, { exitCode: 1 });
|
|
1349
945
|
}
|
|
1350
946
|
return rows;
|
|
1351
947
|
}
|
|
@@ -6311,7 +5907,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6311
5907
|
}));
|
|
6312
5908
|
program
|
|
6313
5909
|
.command("tags")
|
|
6314
|
-
.description(`Workspace tags: one vocabulary across ${TAG_KINDS_PROSE}. Tags normalize to lowercase (trimmed, deduped, max 50 per item; any characters — nothing is slugified, so "Q3 Outbound" becomes "q3 outbound"). ATTACH tags on the owning surface — each of those groups has its own \`tag\` subcommand (e.g. \`oxygen inbox tag\`, \`oxygen tables tag\`, \`oxygen domains tag\`) — then \`tags get <tag>\` returns every carrier with deep-links. CURATE the vocabulary here: \`tags create\` declares a tag (with a description, color, and pin) before anything carries it, \`tags update\` re-annotates one, and \`tags rename\`/\`merge\`/\`delete\` reshape it everywhere at once.`)
|
|
5910
|
+
.description(`Workspace tags: one vocabulary across ${TAG_KINDS_PROSE}. Tags normalize to lowercase (trimmed, deduped, max 50 per item; any characters — nothing is slugified, so "Q3 Outbound" becomes "q3 outbound"). ATTACH tags on the owning surface — each of those groups has its own \`tag\` subcommand (e.g. \`oxygen inbox tag\`, \`oxygen tables tag\`, \`oxygen domains tag\`) — then \`tags get <tag>\` returns every carrier with deep-links. CURATE the vocabulary here: \`tags create\` declares a tag (with a description, color, and pin) before anything carries it, \`tags update\` re-annotates one, and \`tags rename\`/\`merge\`/\`delete\` reshape it everywhere at once. Tags are free, inert metadata: attaching or removing one costs 0 credits, makes no provider call, and changes no sending, routing, or warm-up behaviour.`)
|
|
6315
5911
|
.addCommand(new Command("list")
|
|
6316
5912
|
.description("List every workspace tag with per-primitive counts, most-used first.")
|
|
6317
5913
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8541,6 +8137,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8541
8137
|
// appearing in a command's flag strings, so an approval-shaped name
|
|
8542
8138
|
// here would mislabel every command that carries it.
|
|
8543
8139
|
.option("--move-existing", "Also re-home the mailboxes you already have onto the new dedicated IP. Off by default because moving an established mailbox throws away the sending warmup it accumulated on its current IP.")
|
|
8140
|
+
.option("--country <code>", "ISO 3166-1 alpha-2 country the dedicated IP should sit in (e.g. US, DE). Match it to where your mailboxes' owners plausibly sign in from — the IP's job is making the sign-in look ordinary, and an account that suddenly authenticates from another country is what gets it challenged. Omitted uses the default region. An unserviceable or out-of-stock country fails before anything is ordered or charged.")
|
|
8544
8141
|
.option("--json", "Print a JSON envelope.")
|
|
8545
8142
|
.action(async (options) => {
|
|
8546
8143
|
await handleAsyncAction("egress dedicated request", options, () => requestOxygen("/api/cli/egress/dedicated", {
|
|
@@ -8548,6 +8145,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8548
8145
|
body: {
|
|
8549
8146
|
...(options.approved ? { approve: true } : {}),
|
|
8550
8147
|
...(options.moveExisting ? { move_existing: true } : {}),
|
|
8148
|
+
...(readOption(options.country) ? { country: readOption(options.country) } : {}),
|
|
8551
8149
|
},
|
|
8552
8150
|
}));
|
|
8553
8151
|
}))
|
|
@@ -10075,9 +9673,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10075
9673
|
});
|
|
10076
9674
|
}))
|
|
10077
9675
|
.addCommand(new Command("destinations")
|
|
10078
|
-
.description("List where a connected integration can send — for Microsoft Teams,
|
|
9676
|
+
.description("List where a connected integration can send — for Microsoft Teams, your teams and your chats, plus one team's channels with --team-id. Free: these are read-only lookups, so no approval or credit cap is needed.")
|
|
10079
9677
|
.argument("<integration_id>", "Integration id. Supported: 'microsoft_teams'.")
|
|
10080
|
-
.option("--team-id <id>", "List this team's channels as well
|
|
9678
|
+
.option("--team-id <id>", "List this team's channels as well.")
|
|
9679
|
+
.option("--kind <kind>", "Narrow to one of: teams, channels, chats. Default lists teams and chats.")
|
|
10081
9680
|
.option("--connection-id <id>", "Use a specific connection when several are active.")
|
|
10082
9681
|
.option("--json", "Print a JSON envelope.")
|
|
10083
9682
|
.action(async (integrationId, options) => {
|
|
@@ -10086,6 +9685,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10086
9685
|
const teamId = readOption(options.teamId);
|
|
10087
9686
|
if (teamId)
|
|
10088
9687
|
params.set("team_id", teamId);
|
|
9688
|
+
const kind = readOption(options.kind);
|
|
9689
|
+
if (kind)
|
|
9690
|
+
params.set("kind", kind);
|
|
10089
9691
|
const connectionId = readOption(options.connectionId);
|
|
10090
9692
|
if (connectionId)
|
|
10091
9693
|
params.set("connection_id", connectionId);
|
|
@@ -10129,6 +9731,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10129
9731
|
.description("List connected LinkedIn sender accounts with health status, rate limits, and today's usage.")
|
|
10130
9732
|
.option("--status <status>", "Filter by sender status: active, paused, disconnected, restricted, or credentials_required.")
|
|
10131
9733
|
.option("--no-usage", "Skip today's per-account usage counts for a faster, lighter response.")
|
|
9734
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches sender accounts carrying ANY of these tags (see `oxygen tags list`).")
|
|
10132
9735
|
.option("--json", "Print a JSON envelope.")
|
|
10133
9736
|
.action(async (options) => {
|
|
10134
9737
|
await handleAsyncAction("senders list", options, () => {
|
|
@@ -10138,6 +9741,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10138
9741
|
const status = readOption(options.status);
|
|
10139
9742
|
if (status)
|
|
10140
9743
|
params.set("status", status);
|
|
9744
|
+
const tags = splitCommaList(options.tag);
|
|
9745
|
+
if (tags.length > 0)
|
|
9746
|
+
params.set("tag", tags.join(","));
|
|
10141
9747
|
const suffix = params.toString();
|
|
10142
9748
|
return requestOxygen(`/api/cli/senders${suffix ? `?${suffix}` : ""}`);
|
|
10143
9749
|
});
|
|
@@ -10313,6 +9919,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10313
9919
|
.option("--total-reads-per-day <n>", "Daily cap across all read units.")
|
|
10314
9920
|
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
10315
9921
|
.option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
|
|
9922
|
+
.option("--interactive-min-spacing-seconds <n>", "Minimum seconds between human-sent Unibox actions.")
|
|
9923
|
+
.option("--interactive-spacing-jitter-seconds <n>", "Random jitter seconds added to human-sent Unibox action spacing.")
|
|
9924
|
+
.option("--interactive-sends-per-hour <n>", "Hourly cap on human-sent Unibox actions.")
|
|
10316
9925
|
.option("--timezone <tz>", "IANA timezone the daily action counters reset in, e.g. America/New_York.")
|
|
10317
9926
|
.option("--warmup-restart", "Start (or restart) the warm-up ramp now — gradually raises this account's invite + message caps to full over ~2 weeks.")
|
|
10318
9927
|
.option("--warmup-disable", "Turn off warm-up for this account (treat it as already warm and use its full configured caps).")
|
|
@@ -10335,12 +9944,31 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10335
9944
|
.addCommand(new Command("list")
|
|
10336
9945
|
.description("List sender profiles with their per-channel account counts (LinkedIn / WhatsApp / inboxes).")
|
|
10337
9946
|
.option("--status <status>", "Filter by status: active, paused, or archived.")
|
|
9947
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches sender profiles carrying ANY of these tags (see `oxygen tags list`).")
|
|
10338
9948
|
.option("--json", "Print a JSON envelope.")
|
|
10339
9949
|
.action(async (options) => {
|
|
10340
9950
|
await handleAsyncAction("senders profiles list", options, () => {
|
|
9951
|
+
const params = new URLSearchParams();
|
|
10341
9952
|
const status = readOption(options.status);
|
|
10342
|
-
|
|
9953
|
+
if (status)
|
|
9954
|
+
params.set("status", status);
|
|
9955
|
+
const tags = splitCommaList(options.tag);
|
|
9956
|
+
if (tags.length > 0)
|
|
9957
|
+
params.set("tag", tags.join(","));
|
|
9958
|
+
const suffix = params.toString();
|
|
9959
|
+
return requestOxygen(`/api/cli/senders/profiles${suffix ? `?${suffix}` : ""}`);
|
|
10343
9960
|
});
|
|
9961
|
+
}))
|
|
9962
|
+
.addCommand(new Command("tag")
|
|
9963
|
+
.description("Replace a sender profile's workspace tags (whole set; `--tags \"\"` clears) — one edit pools the whole sending identity behind a campaign tag. Member accounts keep their own tags. See `oxygen tags list` for the vocabulary.")
|
|
9964
|
+
.argument("<id>", "Sender profile id.")
|
|
9965
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
9966
|
+
.option("--json", "Print a JSON envelope.")
|
|
9967
|
+
.action(async (id, options) => {
|
|
9968
|
+
await handleAsyncAction("senders profiles tag", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}/tags`, {
|
|
9969
|
+
method: "POST",
|
|
9970
|
+
body: { tags: splitCommaList(options.tags) },
|
|
9971
|
+
}));
|
|
10344
9972
|
}))
|
|
10345
9973
|
.addCommand(new Command("get")
|
|
10346
9974
|
.description("Get one sender profile with the LinkedIn/WhatsApp senders and inboxes attached to it.")
|
|
@@ -10864,6 +10492,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10864
10492
|
.description("List connected WhatsApp accounts with health status, warm-up ramp state, limits, and today's usage.")
|
|
10865
10493
|
.option("--status <status>", "Filter by account status: active, paused, disconnected, restricted, or credentials_required.")
|
|
10866
10494
|
.option("--no-usage", "Skip today's per-account usage counts.")
|
|
10495
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches WhatsApp accounts carrying ANY of these tags (see `oxygen tags list`).")
|
|
10867
10496
|
.option("--json", "Print a JSON envelope.")
|
|
10868
10497
|
.action(async (options) => {
|
|
10869
10498
|
await handleAsyncAction("whatsapp accounts", options, () => {
|
|
@@ -10873,6 +10502,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10873
10502
|
const status = readOption(options.status);
|
|
10874
10503
|
if (status)
|
|
10875
10504
|
params.set("status", status);
|
|
10505
|
+
const tags = splitCommaList(options.tag);
|
|
10506
|
+
if (tags.length > 0)
|
|
10507
|
+
params.set("tag", tags.join(","));
|
|
10876
10508
|
const suffix = params.toString();
|
|
10877
10509
|
return requestOxygen(`/api/cli/whatsapp/accounts${suffix ? `?${suffix}` : ""}`);
|
|
10878
10510
|
});
|
|
@@ -12508,12 +12140,31 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12508
12140
|
});
|
|
12509
12141
|
})))
|
|
12510
12142
|
.addCommand(new Command("numbers")
|
|
12511
|
-
.description("The org's dialing pool: list | release. Buying is done from the web shop, which prices and previews the recurring order before it is placed.")
|
|
12143
|
+
.description("The org's dialing pool: list | tag | release. Buying is done from the web shop, which prices and previews the recurring order before it is placed.")
|
|
12512
12144
|
.addCommand(new Command("list")
|
|
12513
|
-
.description("List the org's phone numbers with warm-up state, daily cap, and what each one costs per month.")
|
|
12145
|
+
.description("List the org's phone numbers with warm-up state, daily cap, and what each one costs per month. Optional --tag narrows to one campaign's numbers.")
|
|
12146
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches phone numbers carrying ANY of these tags (see `oxygen tags list`).")
|
|
12514
12147
|
.option("--json", "Print a JSON envelope.")
|
|
12515
12148
|
.action(async (options) => {
|
|
12516
|
-
await handleAsyncAction("voice numbers list", options, () =>
|
|
12149
|
+
await handleAsyncAction("voice numbers list", options, () => {
|
|
12150
|
+
const params = new URLSearchParams();
|
|
12151
|
+
const tags = splitCommaList(options.tag);
|
|
12152
|
+
if (tags.length > 0)
|
|
12153
|
+
params.set("tag", tags.join(","));
|
|
12154
|
+
const suffix = params.toString();
|
|
12155
|
+
return requestOxygen(`/api/cli/voice/numbers${suffix ? `?${suffix}` : ""}`);
|
|
12156
|
+
});
|
|
12157
|
+
}))
|
|
12158
|
+
.addCommand(new Command("tag")
|
|
12159
|
+
.description("Replace a phone number's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the numbers dialing one campaign behind its tag. See `oxygen tags list` for the vocabulary.")
|
|
12160
|
+
.argument("<number>", "Phone number in E.164 (e.g. +14155550142) or its id.")
|
|
12161
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
12162
|
+
.option("--json", "Print a JSON envelope.")
|
|
12163
|
+
.action(async (number, options) => {
|
|
12164
|
+
await handleAsyncAction("voice numbers tag", options, () => requestOxygen(`/api/cli/voice/numbers/${encodeURIComponent(number)}/tags`, {
|
|
12165
|
+
method: "POST",
|
|
12166
|
+
body: { tags: splitCommaList(options.tags) },
|
|
12167
|
+
}));
|
|
12517
12168
|
}))
|
|
12518
12169
|
.addCommand(new Command("release")
|
|
12519
12170
|
.description("Give a number back to the carrier and stop its monthly charge. IRREVERSIBLE — the number returns to the carrier's pool and can be taken by someone else within minutes; you cannot get it back. Previews by default; pass --approved to actually release.")
|
|
@@ -12984,10 +12635,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12984
12635
|
});
|
|
12985
12636
|
})));
|
|
12986
12637
|
program.addCommand(new Command("mailboxes")
|
|
12987
|
-
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed SendKit warmup with explicit plans and credit caps. 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).")
|
|
12638
|
+
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed SendKit warmup with explicit plans and credit caps. Managed InboxKit Google and Microsoft mailboxes use InboxKit's native Sequencer export during the approved warmup action; SendKit warms them but never owns campaign dispatch. 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).")
|
|
12988
12639
|
.addCommand(new Command("list")
|
|
12989
12640
|
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), and a pool overview (including counts by source).")
|
|
12990
12641
|
.option("--status <status>", "Filter by status: active, paused, or disabled.")
|
|
12642
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches mailboxes carrying ANY of these tags (see `oxygen tags list`).")
|
|
12991
12643
|
.option("--json", "Print a JSON envelope.")
|
|
12992
12644
|
.action(async (options) => {
|
|
12993
12645
|
await handleAsyncAction("mailboxes list", options, () => {
|
|
@@ -12995,6 +12647,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12995
12647
|
const status = readOption(options.status);
|
|
12996
12648
|
if (status)
|
|
12997
12649
|
params.set("status", status);
|
|
12650
|
+
const tags = splitCommaList(options.tag);
|
|
12651
|
+
if (tags.length > 0)
|
|
12652
|
+
params.set("tag", tags.join(","));
|
|
12998
12653
|
const suffix = params.toString();
|
|
12999
12654
|
return requestOxygen(`/api/cli/mailboxes${suffix ? `?${suffix}` : ""}`);
|
|
13000
12655
|
});
|
|
@@ -13035,7 +12690,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13035
12690
|
});
|
|
13036
12691
|
}))
|
|
13037
12692
|
.addCommand(new Command("import")
|
|
13038
|
-
.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>; --from hypertide remains a shortcut. 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: Microsoft warmup
|
|
12693
|
+
.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>; --from hypertide remains a shortcut. 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: managed InboxKit Google/Microsoft warmup uses native InboxKit Sequencer export during `warmup enable`; eligible non-InboxKit Microsoft warmup uses the separate Outlook OAuth fallback (`mailboxes warmup microsoft`); EmailGuard remains vendor_blocked for Microsoft. Google app passwords are sent only in the request body, encrypted server-side, and never returned.")
|
|
13039
12694
|
.addHelpText("after", [
|
|
13040
12695
|
"",
|
|
13041
12696
|
"Ordinary identity file contract (CSV / JSON / JSONL / XLSX):",
|
|
@@ -13110,7 +12765,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13110
12765
|
: from === "credentials"
|
|
13111
12766
|
? "credential_file"
|
|
13112
12767
|
: "identity_file",
|
|
13113
|
-
sourceProvider,
|
|
12768
|
+
sourceProvider: from === "hypertide" ? "hypertide" : sourceProvider,
|
|
13114
12769
|
});
|
|
13115
12770
|
}
|
|
13116
12771
|
return requestOxygen("/api/cli/mailboxes", {
|
|
@@ -13380,9 +13035,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13380
13035
|
});
|
|
13381
13036
|
}))
|
|
13382
13037
|
.addCommand(new Command("warmup")
|
|
13383
|
-
.description("Mailbox warmup for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. Oxygen's native warmup runs on ONE rail: SendKit, managed and credit-billed at 3,000 credits per warming inbox per month ($3). Oxygen enrolls the mailbox for you — preview first, then re-run with --approved to execute and bill. Google and Microsoft
|
|
13038
|
+
.description("Mailbox warmup for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. Oxygen's native warmup runs on ONE rail: SendKit, managed and credit-billed at 3,000 credits per warming inbox per month ($3). Oxygen enrolls the mailbox for you — preview first, then re-run with --approved to execute and bill. Managed InboxKit Google and Microsoft mailboxes are exported through InboxKit's native Sequencer integration inside that approved action; InboxKit auto-export stays off, and any non-cancelled InboxKit warmup blocks the handoff so one mailbox cannot warm twice. SendKit is warmup-only: 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 here with `warmup disable`; they are never silently moved onto SendKit.")
|
|
13384
13039
|
.addCommand(new Command("enable")
|
|
13385
|
-
.description("Enroll sending mailboxes in Oxygen's managed SendKit warmup and stamp each mailbox's warmup state. Targets the whole pool unless --mailboxes is given. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is enrolled or billed. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Microsoft
|
|
13040
|
+
.description("Enroll sending mailboxes in Oxygen's managed SendKit warmup and stamp each mailbox's warmup state. 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 exported, enrolled, or billed. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Managed InboxKit Google and Microsoft mailboxes use InboxKit's native Sequencer export here by default; auto-export remains off, and any non-cancelled InboxKit warmup must be cancelled before export (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. SendKit supplies warmup only — OXYGEN Sequences retain campaign enrollment and dispatch. New enrollments only ever land on SendKit; the retired TrulyInbox rail is refused here and only accepts teardown.")
|
|
13386
13041
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
13387
13042
|
.option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
|
|
13388
13043
|
.option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
|
|
@@ -13426,8 +13081,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13426
13081
|
});
|
|
13427
13082
|
}))
|
|
13428
13083
|
.addCommand(new Command("microsoft")
|
|
13429
|
-
.description("
|
|
13430
|
-
.option("--mailboxes <list>", "Comma-separated Microsoft mailbox ids or addresses. Omit to select
|
|
13084
|
+
.description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for SendKit warmup with ONE browser consent PER MAILBOX, never a password or app password. Do not run this for managed InboxKit mailboxes: both Google and Microsoft use InboxKit's native Sequencer export during the separately approved `warmup enable`. For the 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 `warmup enable`.")
|
|
13085
|
+
.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.")
|
|
13431
13086
|
.option("--tenant <id>", "Deprecated and ignored: SendKit consent is per mailbox, so there is no tenant-wide scope to bind. Still accepted so older scripts keep running.")
|
|
13432
13087
|
.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.")
|
|
13433
13088
|
.option("--plan <hash>", "Fresh setup preview hash (required with --approved).")
|
|
@@ -13678,6 +13333,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13678
13333
|
.option("--expiring", "Only non-deleted domains expiring within 60 days.")
|
|
13679
13334
|
.option("--has-mailboxes <bool>", "true → only domains with sending mailboxes; false → only domains without.")
|
|
13680
13335
|
.option("--include-archived", "Include archived domains (hidden by default).")
|
|
13336
|
+
.option("--tag <tags>", "Comma-separated workspace tags — matches domains carrying ANY of these tags (see `oxygen tags list`).")
|
|
13681
13337
|
.option("--json", "Print a JSON envelope.")
|
|
13682
13338
|
.action(async (options) => {
|
|
13683
13339
|
await handleAsyncAction("domains list", options, () => {
|
|
@@ -13701,6 +13357,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13701
13357
|
params.set("has_mailboxes", hasMailboxes);
|
|
13702
13358
|
if (options.includeArchived)
|
|
13703
13359
|
params.set("include_archived", "true");
|
|
13360
|
+
const tags = splitCommaList(options.tag);
|
|
13361
|
+
if (tags.length > 0)
|
|
13362
|
+
params.set("tag", tags.join(","));
|
|
13704
13363
|
const suffix = params.toString();
|
|
13705
13364
|
return requestOxygen(`/api/cli/domains${suffix ? `?${suffix}` : ""}`);
|
|
13706
13365
|
});
|
|
@@ -21731,6 +21390,9 @@ options) {
|
|
|
21731
21390
|
setLimit("total_reads_per_day", options.totalReadsPerDay);
|
|
21732
21391
|
setLimit("min_action_spacing_seconds", options.minSpacingSeconds);
|
|
21733
21392
|
setLimit("action_spacing_jitter_seconds", options.spacingJitterSeconds);
|
|
21393
|
+
setLimit("interactive_min_spacing_seconds", options.interactiveMinSpacingSeconds);
|
|
21394
|
+
setLimit("interactive_spacing_jitter_seconds", options.interactiveSpacingJitterSeconds);
|
|
21395
|
+
setLimit("interactive_sends_per_hour", options.interactiveSendsPerHour);
|
|
21734
21396
|
// Send windows are campaign-scoped; the only per-account time setting is the
|
|
21735
21397
|
// timezone the daily counters reset in.
|
|
21736
21398
|
const workingHours = {};
|