@oxygen-agent/cli 1.982.3 → 1.1003.12

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.
Files changed (127) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +0 -2
  3. package/dist/admin-primary-providers-render.js +1 -1
  4. package/dist/browser-login.js +1 -4
  5. package/dist/command-manifest.d.ts +3 -2
  6. package/dist/command-manifest.js +10 -0
  7. package/dist/credentials.d.ts +1 -1
  8. package/dist/functions-commands.js +27 -7
  9. package/dist/help.d.ts +29 -0
  10. package/dist/help.js +139 -0
  11. package/dist/index.js +1875 -164
  12. package/dist/knowledge-mirror.d.ts +2 -2
  13. package/dist/runtime.d.ts +0 -15
  14. package/dist/runtime.js +1 -1
  15. package/dist/session.d.ts +4 -3
  16. package/dist/skills.d.ts +8 -7
  17. package/dist/skills.js +24 -10
  18. package/dist/transcript.d.ts +2 -1
  19. package/dist/ugc-commands.d.ts +3 -6
  20. package/dist/ugc-commands.js +2 -1200
  21. package/dist/util.d.ts +1 -1
  22. package/dist/util.js +1 -3
  23. package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
  24. package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
  25. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
  26. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
  27. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
  28. package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
  29. package/node_modules/@oxygen/cli-ugc/package.json +15 -0
  30. package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
  31. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  32. package/node_modules/@oxygen/formula/dist/expression.js +14 -1
  33. package/node_modules/@oxygen/formula/dist/formula-functions.js +136 -1
  34. package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
  35. package/node_modules/@oxygen/formula/dist/hash.js +199 -0
  36. package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
  37. package/node_modules/@oxygen/formula/dist/index.js +1 -0
  38. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +74 -0
  39. package/node_modules/@oxygen/formula/dist/value-cleaners.js +358 -0
  40. package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
  41. package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
  42. package/node_modules/@oxygen/shared/dist/billing.d.ts +103 -47
  43. package/node_modules/@oxygen/shared/dist/billing.js +150 -40
  44. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -0
  45. package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -16
  46. package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
  47. package/node_modules/@oxygen/shared/dist/column-autofill.js +80 -0
  48. package/node_modules/@oxygen/shared/dist/column-output-fields.js +14 -10
  49. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +108 -0
  50. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +545 -0
  51. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +18 -0
  52. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
  53. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
  54. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
  55. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
  56. package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
  57. package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
  58. package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
  59. package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
  60. package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
  61. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +103 -0
  62. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +819 -0
  63. package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
  64. package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
  65. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
  66. package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
  67. package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
  68. package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
  69. package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
  70. package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
  71. package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
  72. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
  73. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
  74. package/node_modules/@oxygen/shared/dist/index.d.ts +14 -0
  75. package/node_modules/@oxygen/shared/dist/index.js +14 -0
  76. package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
  77. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
  78. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +17 -2
  79. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +28 -6
  80. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +12 -1
  81. package/node_modules/@oxygen/shared/dist/langfuse.js +57 -8
  82. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +32 -0
  83. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +359 -0
  84. package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
  85. package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
  86. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  87. package/node_modules/@oxygen/shared/dist/log.js +6 -1
  88. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
  89. package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
  90. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +54 -0
  91. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +213 -0
  92. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
  93. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
  94. package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
  95. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
  96. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
  97. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
  98. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
  99. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
  100. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +15 -0
  101. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +15 -0
  102. package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
  103. package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
  104. package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +33 -1
  105. package/node_modules/@oxygen/shared/dist/research-output-contract.js +65 -5
  106. package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
  107. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  108. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
  109. package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
  110. package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
  111. package/node_modules/@oxygen/shared/dist/sequences.d.ts +49 -0
  112. package/node_modules/@oxygen/shared/dist/sequences.js +134 -4
  113. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
  114. package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
  115. package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
  116. package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
  117. package/node_modules/@oxygen/shared/dist/telemetry.js +9 -1
  118. package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
  119. package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
  120. package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
  121. package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
  122. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  123. package/node_modules/@oxygen/shared/package.json +60 -0
  124. package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
  125. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
  126. package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
  127. package/package.json +6 -3
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Deterministic value cleaners for dirty GTM strings — company names, job
3
+ * titles, delimited lists, and email-address classification.
4
+ *
5
+ * Pure string functions with no I/O, no tenant context, and no dependencies,
6
+ * so the exact semantics that clean a cell in the worker also run in the
7
+ * browser (formula editor previews). Every list below is a fixed, auditable
8
+ * data set: nothing here calls a provider, spends a credit, or guesses with a
9
+ * model. When a deterministic list is not enough, the paid verifier columns
10
+ * are the upgrade — these helpers are the free first pass.
11
+ */
12
+ import { PERSONAL_EMAIL_DOMAINS } from "@oxygen/shared/freemail-domains";
13
+ // ---------------------------------------------------------------------------
14
+ // Shared text helpers
15
+ // ---------------------------------------------------------------------------
16
+ /** Zero-width joiners, variation selectors, and the BOM — invisible, never meaningful in a cell. */
17
+ const INVISIBLE_PATTERN = /[​-‍⁠︎️⃣]/g;
18
+ /**
19
+ * Emoji, pictographs, and symbol runs. Deliberately Unicode-property based:
20
+ * a byte-range or "non-ASCII" filter would eat accented letters, and GTM data
21
+ * is full of Müller, Ørsted, and Škoda.
22
+ */
23
+ const PICTOGRAPH_PATTERN = /[\p{Extended_Pictographic}\p{Emoji_Presentation}\p{Emoji_Modifier}\p{So}]/gu;
24
+ /** Leading/trailing punctuation, symbols, quotes, and whitespace. Inner punctuation is preserved. */
25
+ const OUTER_NOISE_PATTERN = /^[\s\p{P}\p{S}]+|[\s\p{P}\p{S}]+$/gu;
26
+ function toText(value) {
27
+ if (value === null || value === undefined)
28
+ return "";
29
+ if (typeof value === "string")
30
+ return value;
31
+ if (typeof value === "number" || typeof value === "boolean")
32
+ return String(value);
33
+ try {
34
+ return JSON.stringify(value) ?? "";
35
+ }
36
+ catch {
37
+ return "";
38
+ }
39
+ }
40
+ function collapseWhitespace(value) {
41
+ return value.replace(INVISIBLE_PATTERN, "").replace(/\s+/gu, " ").trim();
42
+ }
43
+ function trimOuterNoise(value) {
44
+ return value.replace(OUTER_NOISE_PATTERN, "");
45
+ }
46
+ /** True when every cased letter in the text is upper-case (digits/punctuation ignored). */
47
+ export function isAllCaps(value) {
48
+ let sawCasedLetter = false;
49
+ for (const char of value) {
50
+ if (!/\p{L}/u.test(char))
51
+ continue;
52
+ const lower = char.toLowerCase();
53
+ const upper = char.toUpperCase();
54
+ if (lower === upper)
55
+ continue; // caseless script (CJK, etc.) — not evidence either way
56
+ sawCasedLetter = true;
57
+ if (char !== upper)
58
+ return false;
59
+ }
60
+ return sawCasedLetter;
61
+ }
62
+ /** Capitalizes the first letter of every word and lowercases the rest (Unicode-aware). */
63
+ export function titleCaseWords(value) {
64
+ return value.replace(/(\p{L})(\p{L}*)/gu, (_match, first, rest) => first.toUpperCase() + rest.toLowerCase());
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Company names
68
+ // ---------------------------------------------------------------------------
69
+ /**
70
+ * Legal/corporate suffixes, stored as punctuation-free lower-case keys so
71
+ * "L.L.C.", "llc", and "LLC," all reduce to `llc`. Multi-word suffixes are
72
+ * joined ("pty ltd" → `ptyltd`) and matched against the trailing 1–3 tokens.
73
+ */
74
+ export const COMPANY_LEGAL_SUFFIX_KEYS = new Set([
75
+ "inc", "incorporated", "corp", "corporation", "co", "company",
76
+ "ltd", "limited", "llc", "llp", "lp", "plc",
77
+ "gmbh", "ag", "sa", "sas", "sarl", "srl", "spa",
78
+ "bv", "nv", "oy", "ab", "as", "pty", "kg", "kk", "pte",
79
+ "sdnbhd", "ptyltd", "pteltd",
80
+ "ltda", "ug", "ev", "ou", "sro", "spzoo", "zrt", "kft", "doo",
81
+ "aps", "hf", "ehf", "ltee",
82
+ ]);
83
+ /** The longest suffix key measured in whitespace-separated tokens ("sp. z o.o."). */
84
+ const MAX_SUFFIX_TOKENS = 3;
85
+ /** Separators after which a company string turns into a tagline, not a name. */
86
+ const SHORT_FORM_SEPARATORS = [" - ", " – ", " — ", " | ", ":", " / ", "("];
87
+ const SHORT_FORM_MAX_WORDS = 3;
88
+ function suffixKey(token) {
89
+ return token.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
90
+ }
91
+ function stripLegalSuffixes(tokens) {
92
+ let remaining = [...tokens];
93
+ // Bounded: every pass removes at least one token, so the token count is the budget.
94
+ for (let pass = 0; pass < tokens.length; pass += 1) {
95
+ while (remaining.length > 1 && suffixKey(remaining[remaining.length - 1] ?? "") === "") {
96
+ remaining = remaining.slice(0, -1);
97
+ }
98
+ let matched = false;
99
+ for (let span = Math.min(MAX_SUFFIX_TOKENS, remaining.length - 1); span >= 1; span -= 1) {
100
+ const key = remaining.slice(remaining.length - span).map(suffixKey).join("");
101
+ if (!key || !COMPANY_LEGAL_SUFFIX_KEYS.has(key))
102
+ continue;
103
+ remaining = remaining.slice(0, remaining.length - span);
104
+ matched = true;
105
+ break;
106
+ }
107
+ if (!matched)
108
+ break;
109
+ }
110
+ return remaining;
111
+ }
112
+ function cutAtShortFormSeparator(value) {
113
+ let cut = value.length;
114
+ for (const separator of SHORT_FORM_SEPARATORS) {
115
+ const index = value.indexOf(separator);
116
+ if (index > 0 && index < cut)
117
+ cut = index;
118
+ }
119
+ return value.slice(0, cut);
120
+ }
121
+ /**
122
+ * Canonical company name: legal suffixes removed, stray punctuation and quotes
123
+ * trimmed, whitespace collapsed. Casing is the author's unless the whole name
124
+ * shouts ("ACME CORP." → "Acme"), because "hubspot" is how that company writes
125
+ * itself. `short` additionally cuts a tagline after a separator and keeps at
126
+ * most three words — the form that fits in an email's first sentence.
127
+ *
128
+ * Blank input returns null.
129
+ */
130
+ export function normalizeCompanyName(value, mode = "display") {
131
+ const raw = collapseWhitespace(toText(value).replace(PICTOGRAPH_PATTERN, ""));
132
+ if (!raw)
133
+ return null;
134
+ const base = mode === "short" ? collapseWhitespace(cutAtShortFormSeparator(raw)) : raw;
135
+ if (!base)
136
+ return null;
137
+ const tokens = stripLegalSuffixes(base.split(" ").filter(Boolean));
138
+ let cleaned = collapseWhitespace(trimOuterNoise(tokens.join(" ")));
139
+ if (!cleaned)
140
+ cleaned = collapseWhitespace(trimOuterNoise(base));
141
+ if (!cleaned)
142
+ return null;
143
+ if (mode === "short") {
144
+ const words = cleaned.split(" ").filter(Boolean);
145
+ if (words.length > SHORT_FORM_MAX_WORDS) {
146
+ cleaned = words.slice(0, SHORT_FORM_MAX_WORDS).join(" ");
147
+ }
148
+ }
149
+ return isAllCaps(cleaned) ? titleCaseWords(cleaned) : cleaned;
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // Job titles
153
+ // ---------------------------------------------------------------------------
154
+ /**
155
+ * Recruiting and self-promotion phrases that ride along in a scraped headline.
156
+ * Longest first so "we're hiring" is consumed before the bare "hiring".
157
+ */
158
+ export const JOB_TITLE_SLOGANS = [
159
+ "we are hiring",
160
+ "we're hiring",
161
+ "join our team",
162
+ "open to work",
163
+ "let's connect",
164
+ "looking for",
165
+ "now hiring",
166
+ "opentowork",
167
+ "join us",
168
+ "dm me",
169
+ "hiring",
170
+ ];
171
+ const SLOGAN_PATTERNS = JOB_TITLE_SLOGANS
172
+ .slice()
173
+ .sort((a, b) => b.length - a.length)
174
+ .map((phrase) => {
175
+ const body = phrase
176
+ .split("")
177
+ .map((char) => {
178
+ if (char === " ")
179
+ return "\\s+";
180
+ if (char === "'")
181
+ return "['’‘ʼ´]";
182
+ return char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
183
+ })
184
+ .join("");
185
+ // No lookbehind: Safari shipped it late and this engine runs in the browser.
186
+ return new RegExp(`(^|[^\\p{L}\\p{N}])(?:${body})(?![\\p{L}\\p{N}])`, "giu");
187
+ });
188
+ /**
189
+ * Separators that join a real title to a second clause. A bare `-`, `,`, `/`,
190
+ * or `&` is NOT one of them: "Account Executive - EMEA", "Sr. Director, Demand
191
+ * Generation", and "Director of Importing/Exporting" are whole titles.
192
+ */
193
+ const JOB_TITLE_SEPARATOR_PATTERN = /\s*\|\s*|\s*[•·]\s*|\s*::\s*|\s+[—–]\s+/u;
194
+ /**
195
+ * Canonical job title: emoji, hashtags, parentheticals, and hiring slogans
196
+ * removed, then only the first clause of a separator-joined headline kept.
197
+ * All-caps titles are title-cased; every other casing is the author's.
198
+ *
199
+ * Blank input, or input that was nothing but noise, returns null.
200
+ */
201
+ export function cleanJobTitle(value) {
202
+ let text = toText(value);
203
+ if (!text.trim())
204
+ return null;
205
+ text = text.replace(PICTOGRAPH_PATTERN, "").replace(INVISIBLE_PATTERN, "");
206
+ // Parentheticals, innermost-out; bounded so a pathological paste cannot spin.
207
+ for (let pass = 0; pass < 4; pass += 1) {
208
+ const next = text.replace(/\([^()]*\)/g, " ").replace(/\[[^[\]]*\]/g, " ");
209
+ if (next === text)
210
+ break;
211
+ text = next;
212
+ }
213
+ text = text.replace(/#[\p{L}\p{N}_-]+/gu, " ");
214
+ for (const pattern of SLOGAN_PATTERNS)
215
+ text = text.replace(pattern, "$1");
216
+ text = collapseWhitespace(text);
217
+ const segments = text.split(JOB_TITLE_SEPARATOR_PATTERN).map((part) => (part ?? "").trim());
218
+ const chosen = segments.find((part) => /[\p{L}\p{N}]/u.test(part)) ?? segments[0] ?? "";
219
+ const cleaned = collapseWhitespace(trimOuterNoise(chosen));
220
+ if (!cleaned)
221
+ return null;
222
+ return isAllCaps(cleaned) ? titleCaseWords(cleaned) : cleaned;
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // Delimited lists
226
+ // ---------------------------------------------------------------------------
227
+ /** Tried in order when no separator is given; the first one present wins. */
228
+ const LIST_DELIMITERS = ["\n", ";", "|", ","];
229
+ function cleanListParts(parts) {
230
+ const seen = new Set();
231
+ const out = [];
232
+ for (const part of parts) {
233
+ const text = collapseWhitespace(toText(part));
234
+ if (!text || seen.has(text))
235
+ continue;
236
+ seen.add(text);
237
+ out.push(text);
238
+ }
239
+ return out;
240
+ }
241
+ /**
242
+ * Turns one messy cell into a clean array: trimmed, blank-free, de-duplicated
243
+ * in first-seen order. An existing array or a JSON-encoded array is cleaned in
244
+ * place; a string is split on the given separator, or on the first delimiter
245
+ * actually present (newline, `;`, `|`, `,`) when none is given.
246
+ *
247
+ * Blank input returns an empty array.
248
+ */
249
+ export function splitDelimitedList(value, separator) {
250
+ if (Array.isArray(value))
251
+ return cleanListParts(value);
252
+ if (value === null || value === undefined)
253
+ return [];
254
+ const text = typeof value === "string" ? value : toText(value);
255
+ const trimmed = text.trim();
256
+ if (!trimmed)
257
+ return [];
258
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
259
+ try {
260
+ const parsed = JSON.parse(trimmed);
261
+ if (Array.isArray(parsed))
262
+ return cleanListParts(parsed);
263
+ }
264
+ catch {
265
+ // Not JSON after all — fall through to delimiter splitting.
266
+ }
267
+ }
268
+ const explicit = typeof separator === "string" ? separator : "";
269
+ if (explicit)
270
+ return cleanListParts(trimmed.split(explicit));
271
+ if (/\r?\n/.test(trimmed))
272
+ return cleanListParts(trimmed.split(/\r?\n/));
273
+ for (const delimiter of LIST_DELIMITERS) {
274
+ if (delimiter !== "\n" && trimmed.includes(delimiter)) {
275
+ return cleanListParts(trimmed.split(delimiter));
276
+ }
277
+ }
278
+ return cleanListParts([trimmed]);
279
+ }
280
+ // ---------------------------------------------------------------------------
281
+ // Email classification
282
+ // ---------------------------------------------------------------------------
283
+ /**
284
+ * Consumer mailbox providers — a person, not a company domain. Re-exported from
285
+ * `@oxygen/shared/freemail-domains`, which is now the ONE list; this module used to
286
+ * carry its own 101-entry copy and was the best-maintained of eight divergent forks.
287
+ * Its entries survive as the bulk of the shared broad tier.
288
+ */
289
+ // Aliased rather than `export … from`, because this module also USES the set locally
290
+ // (see classifyDomain below) and a re-export creates no local binding.
291
+ export const FREE_MAIL_DOMAINS = PERSONAL_EMAIL_DOMAINS;
292
+ /** Throwaway inbox providers — never worth a send, never worth a credit. */
293
+ export const DISPOSABLE_EMAIL_DOMAINS = new Set([
294
+ "mailinator.com", "guerrillamail.com", "guerrillamail.net",
295
+ "10minutemail.com", "10minutemail.net",
296
+ "tempmail.com", "temp-mail.org", "temp-mail.io", "tempr.email",
297
+ "yopmail.com", "yopmail.fr", "trashmail.com", "trashmail.me",
298
+ "getnada.com", "dispostable.com", "maildrop.cc", "sharklasers.com",
299
+ "throwawaymail.com", "fakeinbox.com", "mohmal.com", "mailnesia.com",
300
+ "spamgourmet.com", "discard.email", "mintemail.com", "emailondeck.com",
301
+ "burnermail.io", "tempail.com", "mytemp.email", "moakt.com",
302
+ "tmpmail.org", "tmpmail.net", "minutemail.com", "mailcatch.com",
303
+ "spam4.me", "harakirimail.com", "jetable.org", "mailexpire.com",
304
+ "throwam.com", "tempinbox.com", "inboxkitten.com",
305
+ ]);
306
+ /** Shared-inbox local parts — a department, not a person. */
307
+ export const ROLE_EMAIL_LOCAL_PARTS = new Set([
308
+ "info", "sales", "support", "help", "admin", "administrator",
309
+ "contact", "hello", "hi", "team", "office",
310
+ "billing", "invoices", "accounts", "accounting", "finance",
311
+ "marketing", "hr", "jobs", "careers", "recruiting",
312
+ "press", "media", "pr", "legal", "privacy", "security", "abuse",
313
+ "postmaster", "webmaster", "hostmaster",
314
+ "noreply", "no-reply", "donotreply", "do-not-reply",
315
+ "newsletter", "notifications", "notification",
316
+ "mail", "mailer-daemon", "service", "services",
317
+ "customerservice", "customer-service",
318
+ "enquiries", "inquiries", "enquiry", "inquiry",
319
+ "orders", "order", "partners", "partnerships",
320
+ "feedback", "welcome", "community",
321
+ "dev", "devops", "it", "ops", "engineering",
322
+ "alerts", "subscriptions",
323
+ ]);
324
+ const EMAIL_SYNTAX_PATTERN = /^[^\s@,;:<>"'()[\]\\]+@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
325
+ /** A domain matches a list when it IS a listed domain or a subdomain of one (mail.yahoo.com). */
326
+ function matchesDomainList(domain, list) {
327
+ if (list.has(domain))
328
+ return true;
329
+ const labels = domain.split(".");
330
+ for (let index = 1; index < labels.length - 1; index += 1) {
331
+ if (list.has(labels.slice(index).join(".")))
332
+ return true;
333
+ }
334
+ return false;
335
+ }
336
+ /**
337
+ * Classifies an email address from OXYGEN's built-in lists, in precedence
338
+ * order: a throwaway domain wins, then a shared-inbox local part, then a
339
+ * consumer mailbox provider, and anything left is a company address.
340
+ *
341
+ * Blank or syntactically invalid input returns null.
342
+ */
343
+ export function classifyEmailType(value) {
344
+ const email = toText(value).trim().toLowerCase();
345
+ if (!email || !EMAIL_SYNTAX_PATTERN.test(email))
346
+ return null;
347
+ const at = email.lastIndexOf("@");
348
+ const localPart = email.slice(0, at);
349
+ const domain = email.slice(at + 1);
350
+ if (matchesDomainList(domain, DISPOSABLE_EMAIL_DOMAINS))
351
+ return "disposable";
352
+ const untagged = localPart.split("+")[0] ?? localPart;
353
+ if (ROLE_EMAIL_LOCAL_PARTS.has(untagged) || ROLE_EMAIL_LOCAL_PARTS.has(localPart))
354
+ return "role";
355
+ if (matchesDomainList(domain, FREE_MAIL_DOMAINS))
356
+ return "personal";
357
+ return "work";
358
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Small array helpers shared across every OXYGEN package.
3
+ */
4
+ /** Split `values` into consecutive slices of at most `size` entries. */
5
+ export declare function chunk<T>(values: T[], size: number): T[][];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Small array helpers shared across every OXYGEN package.
3
+ */
4
+ /** Split `values` into consecutive slices of at most `size` entries. */
5
+ export function chunk(values, size) {
6
+ const chunks = [];
7
+ for (let index = 0; index < values.length; index += size) {
8
+ chunks.push(values.slice(index, index + size));
9
+ }
10
+ return chunks;
11
+ }