@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.
@@ -0,0 +1,556 @@
1
+ import { OxygenError } from "./cli-result.js";
2
+ export const MAILBOX_IMPORT_FILE_MAX_BYTES = 5 * 1024 * 1024;
3
+ export const MAILBOX_IMPORT_ROW_LIMIT = 500;
4
+ const MAILBOX_EMAIL_HEADERS = new Set([
5
+ "address",
6
+ "email",
7
+ "emailaddress",
8
+ "fromemail",
9
+ "mailbox",
10
+ "mailboxaddress",
11
+ "mailboxemail",
12
+ ]);
13
+ const MAILBOX_PROVIDER_HEADERS = new Set([
14
+ "emailprovider",
15
+ "esp",
16
+ "mailboxprovider",
17
+ "platform",
18
+ "provider",
19
+ "serviceprovider",
20
+ "type",
21
+ ]);
22
+ const MAILBOX_PASSWORD_HEADERS = new Set([
23
+ "apppassword",
24
+ "applicationpassword",
25
+ "googleapppassword",
26
+ "imappassword",
27
+ "mailboxpassword",
28
+ "password",
29
+ "smtppassword",
30
+ ]);
31
+ const MAILBOX_WORKSPACE_HEADERS = new Set([
32
+ "externalaccountid",
33
+ "mailboxid",
34
+ "mailboxuid",
35
+ "sourceaccountid",
36
+ "sourcemailboxid",
37
+ "uid",
38
+ "workspaceexternalid",
39
+ ]);
40
+ const MAILBOX_PLATFORM_HEADERS = new Set([
41
+ "infrastructure",
42
+ "infrastructureplatform",
43
+ "infraplatform",
44
+ ]);
45
+ const MAILBOX_TENANT_HEADERS = new Set([
46
+ "azuretenantid",
47
+ "entratenantid",
48
+ "microsofttenantid",
49
+ "tenantid",
50
+ ]);
51
+ const MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS = new Set([
52
+ "accesstoken",
53
+ "applicationsecret",
54
+ "authenticatorsecret",
55
+ "authorization",
56
+ "authorizationcode",
57
+ "bearertoken",
58
+ "clientsecret",
59
+ "clientprivatekey",
60
+ "delegationkey",
61
+ "idtoken",
62
+ "mfacode",
63
+ "mfasecret",
64
+ "oauthaccesstoken",
65
+ "oauthcode",
66
+ "oauthrefreshtoken",
67
+ "oauthtoken",
68
+ "onetimepassword",
69
+ "otp",
70
+ "otpsecret",
71
+ "privatekey",
72
+ "refreshtoken",
73
+ "serviceaccountkey",
74
+ "serviceaccountjson",
75
+ "serviceaccountprivatekey",
76
+ "totp",
77
+ "totpsecret",
78
+ ]);
79
+ /**
80
+ * Infer the bounded mailbox-import format without node:path so browser and CLI
81
+ * preflight use the same extension contract.
82
+ */
83
+ export function inferMailboxImportFileFormat(name) {
84
+ const path = name.split(/[?#]/, 1)[0] ?? name;
85
+ const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
86
+ if (extension === ".jsonl" || extension === ".ndjson")
87
+ return "jsonl";
88
+ if (extension === ".csv")
89
+ return "csv";
90
+ if (extension === ".xlsx" || extension === ".xlsm")
91
+ return "xlsx";
92
+ return "json";
93
+ }
94
+ /**
95
+ * Parse credential-sensitive text without ever echoing file contents. JSON may
96
+ * be either a bare array or the common `{ mailboxes: [...] }` export wrapper.
97
+ */
98
+ export function parseMailboxImportText(text, format) {
99
+ if (format === "csv")
100
+ return parseMailboxCsvRows(text);
101
+ if (format === "jsonl") {
102
+ return text
103
+ .split(/\r?\n/)
104
+ .map((line, index) => ({ line: line.trim(), lineNumber: index + 1 }))
105
+ .filter(({ line }) => Boolean(line))
106
+ .map(({ line, lineNumber }) => {
107
+ try {
108
+ return JSON.parse(line);
109
+ }
110
+ catch {
111
+ throw new OxygenError("invalid_jsonl", `Mailbox import contains invalid JSON on line ${lineNumber}. File contents are omitted because they may contain credentials.`, { exitCode: 1 });
112
+ }
113
+ });
114
+ }
115
+ let parsed;
116
+ try {
117
+ parsed = JSON.parse(text);
118
+ }
119
+ catch {
120
+ throw new OxygenError("invalid_json", "Mailbox import must contain valid JSON. File contents are omitted because they may contain credentials.", { exitCode: 1 });
121
+ }
122
+ if (Array.isArray(parsed))
123
+ return parsed;
124
+ if (parsed &&
125
+ typeof parsed === "object" &&
126
+ !Array.isArray(parsed) &&
127
+ Array.isArray(parsed.mailboxes)) {
128
+ return parsed.mailboxes;
129
+ }
130
+ throw new OxygenError("invalid_request", "Mailbox import JSON must contain an array or { mailboxes: [...] }.", { exitCode: 1 });
131
+ }
132
+ /** Convert read-excel-file's browser rows to the same object rows as CSV. */
133
+ export function normalizeMailboxWorkbookRows(rows) {
134
+ const [header, ...body] = rows;
135
+ if (!header || header.length === 0)
136
+ return [];
137
+ const labels = header
138
+ .map((cell, index) => ({
139
+ index,
140
+ label: cell === null || cell === undefined ? "" : String(cell).trim(),
141
+ }));
142
+ assertUniqueMailboxCsvHeaders(labels.map(({ label }) => label));
143
+ for (const [index, row] of body.entries()) {
144
+ if (row.length > header.length) {
145
+ throw new OxygenError("invalid_csv", `Workbook row ${index + 2} contains values beyond the named headers.`, { exitCode: 1 });
146
+ }
147
+ }
148
+ return body
149
+ .filter((row) => row.some((cell) => cell !== null && cell !== undefined && String(cell).trim()))
150
+ .map((row) => Object.fromEntries(labels.map(({ index, label }) => [
151
+ label,
152
+ normalizeMailboxWorkbookCell(row[index]),
153
+ ])));
154
+ }
155
+ /**
156
+ * Whitelist mailbox fields before a local file crosses the network. The same
157
+ * function runs in the CLI and first-party web app, so accepted aliases and
158
+ * non-transferable-secret rejection cannot drift by surface.
159
+ */
160
+ export function normalizeMailboxImportFile(value, mode) {
161
+ if (!Array.isArray(value) || value.length === 0) {
162
+ throw new OxygenError("invalid_request", "The file must contain a non-empty mailboxes array.", { exitCode: 1 });
163
+ }
164
+ if (value.length > MAILBOX_IMPORT_ROW_LIMIT) {
165
+ throw new OxygenError("invalid_request", `Mailbox imports are limited to ${MAILBOX_IMPORT_ROW_LIMIT} rows per request.`, { exitCode: 1 });
166
+ }
167
+ const rows = value.map((entry, index) => {
168
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
169
+ throw new OxygenError("invalid_request", `mailboxes[${index}] must be an object.`, { exitCode: 1 });
170
+ }
171
+ return normalizeMailboxExportRow(entry, index, mode);
172
+ });
173
+ return dedupeMailboxImportRows(rows);
174
+ }
175
+ export function summarizeMailboxImportValidation(mailboxes, input) {
176
+ const providers = { google: 0, microsoft: 0 };
177
+ const infrastructurePlatforms = {};
178
+ let credentialRows = 0;
179
+ for (const mailbox of mailboxes) {
180
+ providers[mailbox.provider] += 1;
181
+ if (mailbox.infrastructure_platform) {
182
+ infrastructurePlatforms[mailbox.infrastructure_platform] =
183
+ (infrastructurePlatforms[mailbox.infrastructure_platform] ?? 0) + 1;
184
+ }
185
+ if (mailbox.app_password)
186
+ credentialRows += 1;
187
+ }
188
+ return {
189
+ valid: true,
190
+ source: input.source,
191
+ ...(input.sourceProvider ? { source_provider: input.sourceProvider } : {}),
192
+ rows: mailboxes.length,
193
+ providers,
194
+ infrastructure_platforms: infrastructurePlatforms,
195
+ credential_rows: credentialRows,
196
+ identity_only_rows: mailboxes.length - credentialRows,
197
+ limits: {
198
+ max_rows: MAILBOX_IMPORT_ROW_LIMIT,
199
+ max_file_bytes: MAILBOX_IMPORT_FILE_MAX_BYTES,
200
+ },
201
+ mutation: false,
202
+ network_request: false,
203
+ provider_call: false,
204
+ credits_used: 0,
205
+ next_action: "Submit this reviewed file to register the mailboxes.",
206
+ };
207
+ }
208
+ export function normalizeMailboxImportVendor(raw, from) {
209
+ if (from === "hypertide") {
210
+ if (raw && raw.trim().toLowerCase() !== "hypertide") {
211
+ throw new OxygenError("invalid_request", "Hypertide import provenance is fixed to hypertide.", { exitCode: 1 });
212
+ }
213
+ return null;
214
+ }
215
+ if (from === "credentials" && !raw) {
216
+ throw new OxygenError("invalid_request", "Name the provider that exported these credentials so each mailbox keeps its provenance.", { exitCode: 1 });
217
+ }
218
+ if (!raw)
219
+ return null;
220
+ const normalized = raw.trim().toLowerCase();
221
+ if (normalized.length < 1 ||
222
+ normalized.length > 64 ||
223
+ !/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(normalized)) {
224
+ throw new OxygenError("invalid_request", "Provider name must be 1-64 lowercase letters, digits, dots, underscores, or hyphens.", { exitCode: 1 });
225
+ }
226
+ return normalized;
227
+ }
228
+ function normalizeMailboxExportRow(row, index, mode) {
229
+ const byHeader = new Map();
230
+ for (const [header, value] of Object.entries(row)) {
231
+ const normalizedHeader = normalizeMailboxExportHeader(header);
232
+ const existing = byHeader.get(normalizedHeader);
233
+ if (existing !== undefined &&
234
+ String(existing).trim() !== String(value ?? "").trim()) {
235
+ throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting columns that normalize to ${normalizedHeader}.`, { exitCode: 1 });
236
+ }
237
+ byHeader.set(normalizedHeader, value);
238
+ }
239
+ const email = readUniqueMailboxExportString(byHeader, MAILBOX_EMAIL_HEADERS, index, "email_address", (value) => value.trim().toLowerCase()) ?? readEmailShapedMailboxUsername(byHeader);
240
+ const providerValues = readMailboxExportStrings(byHeader, MAILBOX_PROVIDER_HEADERS);
241
+ const provider = normalizeMailboxProvider(providerValues, byHeader, index);
242
+ if (!email ||
243
+ email.length > 320 ||
244
+ !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
245
+ throw new OxygenError("invalid_request", `mailboxes[${index}].email_address must be a valid address of at most 320 characters.`, { exitCode: 1 });
246
+ }
247
+ if (!provider) {
248
+ 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 });
249
+ }
250
+ if (hasMailboxExportValue(byHeader, MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS) ||
251
+ hasNonTransferableMailboxAuthLikeValue(byHeader)) {
252
+ 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 consent flow.`, { exitCode: 1 });
253
+ }
254
+ const passwords = readMailboxExportStrings(byHeader, MAILBOX_PASSWORD_HEADERS);
255
+ const distinctPasswords = [...new Set(passwords)];
256
+ if (distinctPasswords.some((password) => password.length > 1024)) {
257
+ throw new OxygenError("invalid_request", `mailboxes[${index}].app_password must be at most 1024 characters.`, { exitCode: 1 });
258
+ }
259
+ if (mode === "identity" && distinctPasswords.length > 0) {
260
+ throw new OxygenError("invalid_request", "This identity-only import contains a password. Choose a secure credential import for a compatible Google app-password export.", { exitCode: 1 });
261
+ }
262
+ if (provider !== "microsoft" && distinctPasswords.length > 1) {
263
+ throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting SMTP/IMAP/app-password values.`, { exitCode: 1 });
264
+ }
265
+ if (mode === "credential" &&
266
+ provider === "google" &&
267
+ distinctPasswords.length === 0) {
268
+ 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 });
269
+ }
270
+ const workspaceExternalId = readUniqueMailboxExportString(byHeader, MAILBOX_WORKSPACE_HEADERS, index, "workspace_external_id", (value) => value.trim());
271
+ if (workspaceExternalId && workspaceExternalId.length > 512) {
272
+ throw new OxygenError("invalid_request", `mailboxes[${index}].workspace_external_id must be at most 512 characters.`, { exitCode: 1 });
273
+ }
274
+ const tenantId = readUniqueMailboxExportString(byHeader, MAILBOX_TENANT_HEADERS, index, "tenant_id", (value) => value.trim().toLowerCase());
275
+ if (tenantId && provider !== "microsoft") {
276
+ throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id applies only to Microsoft mailboxes.`, { exitCode: 1 });
277
+ }
278
+ if (tenantId &&
279
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tenantId)) {
280
+ throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id must be a Microsoft Entra tenant GUID.`, { exitCode: 1 });
281
+ }
282
+ const infrastructurePlatformRaw = readUniqueMailboxExportString(byHeader, MAILBOX_PLATFORM_HEADERS, index, "infrastructure_platform", normalizeMailboxExportHeader) ??
283
+ (providerValues.some((value) => ["azure", "entra", "microsoftazure"].includes(normalizeMailboxExportHeader(value)))
284
+ ? "microsoft_azure"
285
+ : null);
286
+ const infrastructurePlatform = normalizeMailboxInfrastructurePlatform(infrastructurePlatformRaw, provider, index);
287
+ return {
288
+ email_address: email.trim().toLowerCase(),
289
+ provider,
290
+ ...(workspaceExternalId !== null
291
+ ? { workspace_external_id: workspaceExternalId.trim() }
292
+ : {}),
293
+ ...(infrastructurePlatformRaw
294
+ ? { infrastructure_platform: infrastructurePlatform }
295
+ : {}),
296
+ ...(tenantId ? { tenant_id: tenantId.trim().toLowerCase() } : {}),
297
+ ...(mode === "credential" &&
298
+ provider === "google" &&
299
+ distinctPasswords[0] !== undefined
300
+ ? { app_password: distinctPasswords[0] }
301
+ : {}),
302
+ };
303
+ }
304
+ function normalizeMailboxExportHeader(value) {
305
+ return value
306
+ .replace(/^\uFEFF/, "")
307
+ .normalize("NFKC")
308
+ .trim()
309
+ .toLowerCase()
310
+ .replace(/[^a-z0-9]/g, "");
311
+ }
312
+ function readUniqueMailboxExportString(byHeader, headers, index, field, normalize) {
313
+ const values = readMailboxExportStrings(byHeader, headers);
314
+ if (values.length === 0)
315
+ return null;
316
+ const normalized = [...new Set(values.map(normalize))];
317
+ if (normalized.length > 1) {
318
+ throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting ${field} aliases.`, { exitCode: 1 });
319
+ }
320
+ return normalized[0] ?? null;
321
+ }
322
+ function readMailboxExportStrings(byHeader, headers) {
323
+ const values = [];
324
+ for (const header of headers) {
325
+ const value = byHeader.get(header);
326
+ if (typeof value === "string" && value.length > 0)
327
+ values.push(value);
328
+ }
329
+ return values;
330
+ }
331
+ function hasMailboxExportValue(byHeader, headers) {
332
+ for (const header of headers) {
333
+ const value = byHeader.get(header);
334
+ if (value !== undefined && value !== null && String(value).trim())
335
+ return true;
336
+ }
337
+ return false;
338
+ }
339
+ function hasNonTransferableMailboxAuthLikeValue(byHeader) {
340
+ for (const [header, value] of byHeader) {
341
+ if (/(?:token|secret|privatekey|totp|passcode|authorization)/.test(header) &&
342
+ value !== undefined &&
343
+ value !== null &&
344
+ String(value).trim()) {
345
+ return true;
346
+ }
347
+ }
348
+ return false;
349
+ }
350
+ function readEmailShapedMailboxUsername(byHeader) {
351
+ for (const header of ["username", "imapusername", "smtpusername"]) {
352
+ const value = byHeader.get(header);
353
+ if (typeof value === "string" && value.includes("@")) {
354
+ return value.trim().toLowerCase();
355
+ }
356
+ }
357
+ return null;
358
+ }
359
+ function normalizeMailboxProvider(providerValues, byHeader, index) {
360
+ const explicit = providerValues.map((value) => {
361
+ const normalized = normalizeMailboxExportHeader(value);
362
+ if (["google", "googleworkspace", "gmail", "gsuite"].includes(normalized)) {
363
+ return "google";
364
+ }
365
+ if ([
366
+ "azure",
367
+ "entra",
368
+ "m365",
369
+ "microsoft",
370
+ "microsoft365",
371
+ "microsoftazure",
372
+ "ms365",
373
+ "o365",
374
+ "office365",
375
+ "outlook",
376
+ ].includes(normalized)) {
377
+ return "microsoft";
378
+ }
379
+ throw new OxygenError("invalid_request", `mailboxes[${index}].provider is unsupported; expected Google/Gmail/Workspace or Microsoft/Outlook/Office 365/Entra.`, { exitCode: 1 });
380
+ });
381
+ const explicitKinds = [...new Set(explicit)];
382
+ if (explicitKinds.length > 1) {
383
+ throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting provider aliases.`, { exitCode: 1 });
384
+ }
385
+ const hostValues = [
386
+ byHeader.get("imaphost"),
387
+ byHeader.get("imaphostname"),
388
+ byHeader.get("imapserver"),
389
+ byHeader.get("smtphost"),
390
+ byHeader.get("smtphostname"),
391
+ byHeader.get("smtpserver"),
392
+ ].filter((value) => typeof value === "string" && Boolean(value.trim()));
393
+ const hostKinds = hostValues.map((value) => {
394
+ const hostname = normalizeMailboxHost(value);
395
+ if (hostname === "smtp.gmail.com" || hostname === "imap.gmail.com") {
396
+ return "google";
397
+ }
398
+ if (hostname === "smtp.office365.com" ||
399
+ hostname === "outlook.office365.com") {
400
+ return "microsoft";
401
+ }
402
+ 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 });
403
+ });
404
+ const inferredKinds = [...new Set(hostKinds)];
405
+ if (inferredKinds.length > 1) {
406
+ throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting Google and Microsoft mail hosts.`, { exitCode: 1 });
407
+ }
408
+ const explicitKind = explicitKinds[0];
409
+ const inferredKind = inferredKinds[0];
410
+ if (explicitKind && inferredKind && explicitKind !== inferredKind) {
411
+ throw new OxygenError("invalid_request", `mailboxes[${index}] has conflicting provider and SMTP/IMAP host evidence.`, { exitCode: 1 });
412
+ }
413
+ return explicitKind ?? inferredKind ?? null;
414
+ }
415
+ function normalizeMailboxHost(value) {
416
+ const withoutScheme = value
417
+ .trim()
418
+ .toLowerCase()
419
+ .replace(/^[a-z]+:\/\//, "");
420
+ return withoutScheme.split(/[/:]/, 1)[0] ?? "";
421
+ }
422
+ function normalizeMailboxInfrastructurePlatform(raw, provider, index) {
423
+ if (!raw)
424
+ return provider === "google" ? "google_workspace" : "microsoft_365";
425
+ const normalized = normalizeMailboxExportHeader(raw);
426
+ if (["google", "googleworkspace", "gmail", "gsuite", "workspace"].includes(normalized)) {
427
+ if (provider !== "google")
428
+ throw mailboxPlatformConflict(index, provider);
429
+ return "google_workspace";
430
+ }
431
+ if (["azure", "entra", "microsoftazure"].includes(normalized)) {
432
+ if (provider !== "microsoft")
433
+ throw mailboxPlatformConflict(index, provider);
434
+ return "microsoft_azure";
435
+ }
436
+ if ([
437
+ "m365",
438
+ "microsoft",
439
+ "microsoft365",
440
+ "ms365",
441
+ "o365",
442
+ "office365",
443
+ "outlook",
444
+ ].includes(normalized)) {
445
+ if (provider !== "microsoft")
446
+ throw mailboxPlatformConflict(index, provider);
447
+ return "microsoft_365";
448
+ }
449
+ throw new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform is unsupported; expected Google Workspace, Microsoft 365, Azure, or Entra.`, { exitCode: 1 });
450
+ }
451
+ function mailboxPlatformConflict(index, provider) {
452
+ return new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform conflicts with provider=${provider}.`, { exitCode: 1 });
453
+ }
454
+ function dedupeMailboxImportRows(rows) {
455
+ const unique = new Map();
456
+ for (const row of rows) {
457
+ const address = row.email_address.toLowerCase();
458
+ const existing = unique.get(address);
459
+ if (!existing) {
460
+ unique.set(address, row);
461
+ continue;
462
+ }
463
+ if (JSON.stringify(existing) !== JSON.stringify(row)) {
464
+ throw new OxygenError("invalid_request", `Duplicate mailbox ${address} has conflicting provider, platform, tenant, external id, or credential fields.`, { exitCode: 1 });
465
+ }
466
+ }
467
+ return [...unique.values()];
468
+ }
469
+ function parseMailboxCsvRows(text) {
470
+ const records = parseMailboxCsvRecords(text);
471
+ const [header, ...rows] = records;
472
+ if (!header || header.length === 0)
473
+ return [];
474
+ assertUniqueMailboxCsvHeaders(header);
475
+ for (const [index, row] of rows.entries()) {
476
+ if (row.length > header.length) {
477
+ throw new OxygenError("invalid_csv", `CSV row ${index + 2} contains more fields than the header.`, { exitCode: 1 });
478
+ }
479
+ }
480
+ return rows
481
+ .filter((row) => row.some((cell) => cell.trim()))
482
+ .map((row) => Object.fromEntries(header.map((key, index) => [
483
+ key,
484
+ row[index] === undefined || row[index] === "" ? null : row[index],
485
+ ])));
486
+ }
487
+ function parseMailboxCsvRecords(text) {
488
+ const records = [];
489
+ let record = [];
490
+ let field = "";
491
+ let inQuotes = false;
492
+ for (let index = 0; index < text.length; index += 1) {
493
+ const char = text[index];
494
+ const next = text[index + 1];
495
+ if (inQuotes) {
496
+ if (char === '"' && next === '"') {
497
+ field += '"';
498
+ index += 1;
499
+ }
500
+ else if (char === '"') {
501
+ inQuotes = false;
502
+ }
503
+ else {
504
+ field += char;
505
+ }
506
+ continue;
507
+ }
508
+ if (char === '"')
509
+ inQuotes = true;
510
+ else if (char === ",") {
511
+ record.push(field);
512
+ field = "";
513
+ }
514
+ else if (char === "\n") {
515
+ record.push(field);
516
+ records.push(record);
517
+ record = [];
518
+ field = "";
519
+ }
520
+ else if (char !== "\r") {
521
+ field += char;
522
+ }
523
+ }
524
+ if (inQuotes) {
525
+ throw new OxygenError("invalid_csv", "CSV contains an unterminated quoted field.", {
526
+ exitCode: 1,
527
+ });
528
+ }
529
+ if (field || record.length > 0) {
530
+ record.push(field);
531
+ records.push(record);
532
+ }
533
+ return records;
534
+ }
535
+ function assertUniqueMailboxCsvHeaders(header) {
536
+ const seen = new Set();
537
+ for (const value of header) {
538
+ const normalized = value.normalize("NFKC").trim().toLowerCase();
539
+ if (!normalized || seen.has(normalized)) {
540
+ throw new OxygenError("invalid_csv", "CSV/XLSX headers must be non-empty and unique (case-insensitive).", { exitCode: 1 });
541
+ }
542
+ seen.add(normalized);
543
+ }
544
+ }
545
+ function normalizeMailboxWorkbookCell(value) {
546
+ if (value === undefined || value === null || value === "")
547
+ return null;
548
+ if (typeof value === "string" ||
549
+ typeof value === "number" ||
550
+ typeof value === "boolean") {
551
+ return value;
552
+ }
553
+ if (value instanceof Date)
554
+ return value.toISOString();
555
+ return String(value);
556
+ }
@@ -1,6 +1,6 @@
1
1
  import type { TagColor } from "./select-options.js";
2
2
  /** Primitive kinds that participate in the workspace tag union today. */
3
- export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project"];
3
+ export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number"];
4
4
  export type TagKind = (typeof TAG_KINDS)[number];
5
5
  /**
6
6
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -24,6 +24,8 @@ export const TAG_KINDS = [
24
24
  "record",
25
25
  "domain",
26
26
  "project",
27
+ "sender_profile",
28
+ "voice_number",
27
29
  ];
28
30
  /**
29
31
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -37,7 +39,7 @@ export const TAG_KINDS = [
37
39
  */
38
40
  export const TAG_KINDS_PROSE = "publishing posts, knowledge pages, sequences, tables, workflows, recipes, " +
39
41
  "inbox conversations, CRM records, mailboxes, sender accounts, sending domains, " +
40
- "and projects";
42
+ "projects, sender profiles, and phone numbers";
41
43
  /**
42
44
  * The phrase each kind must contribute to {@link TAG_KINDS_PROSE}. Typed as a
43
45
  * total map over TagKind, so adding a kind is a compile error until it is named
@@ -56,6 +58,8 @@ export const TAG_KIND_PROSE_MARKERS = {
56
58
  record: "CRM record",
57
59
  domain: "sending domain",
58
60
  project: "project",
61
+ sender_profile: "sender profile",
62
+ voice_number: "phone number",
59
63
  };
60
64
  export function isTagKind(value) {
61
65
  return typeof value === "string" && TAG_KINDS.includes(value);
@@ -85,6 +89,8 @@ export const TAG_KIND_LABELS = {
85
89
  record: { one: "CRM record", many: "CRM records" },
86
90
  domain: { one: "domain", many: "domains" },
87
91
  project: { one: "project", many: "projects" },
92
+ sender_profile: { one: "sender profile", many: "sender profiles" },
93
+ voice_number: { one: "phone number", many: "phone numbers" },
88
94
  };
89
95
  /** Inline label for a count: `tagKindLabel("sequence", 1)` → "sequence". */
90
96
  export function tagKindLabel(kind, count) {
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.662.0";
1
+ export declare const OXYGEN_VERSION = "1.681.2";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.662.0";
1
+ export const OXYGEN_VERSION = "1.681.2";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
@@ -21,6 +21,11 @@
21
21
  "import": "./dist/file-import.js",
22
22
  "default": "./dist/file-import.js"
23
23
  },
24
+ "./mailbox-import": {
25
+ "types": "./dist/mailbox-import.d.ts",
26
+ "import": "./dist/mailbox-import.js",
27
+ "default": "./dist/mailbox-import.js"
28
+ },
24
29
  "./linkedin-url": {
25
30
  "types": "./dist/linkedin-url.d.ts",
26
31
  "import": "./dist/linkedin-url.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.662.0",
3
+ "version": "1.681.2",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",