@oxygen-agent/cli 1.948.1 → 1.987.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/admin-primary-providers-render.js +9 -1
- package/dist/cli-values.d.ts +14 -0
- package/dist/cli-values.js +26 -0
- package/dist/command-manifest.js +6 -0
- package/dist/functions-commands.js +33 -9
- package/dist/help.d.ts +21 -0
- package/dist/help.js +94 -0
- package/dist/index.js +1493 -297
- package/dist/knowledge-repository-commands.d.ts +6 -0
- package/dist/knowledge-repository-commands.js +198 -0
- package/dist/skills.js +20 -0
- package/dist/ugc-commands.d.ts +3 -6
- package/dist/ugc-commands.js +2 -1086
- package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
- package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
- package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
- package/node_modules/@oxygen/cli-ugc/package.json +15 -0
- package/node_modules/@oxygen/formula/dist/expression.js +14 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.js +71 -1
- package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/formula/dist/index.js +1 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +69 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.js +374 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +27 -27
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +127 -18
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +12 -4
- package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
- package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
- package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/index.js +5 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +32 -38
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +38 -41
- package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
- package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +12 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +48 -8
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +24 -0
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
- package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +33 -1
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +64 -2
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +152 -2
- package/node_modules/@oxygen/shared/dist/sequences.js +304 -4
- package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +3 -1
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
- package/node_modules/@oxygen/shared/package.json +25 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
- package/package.json +6 -2
|
@@ -0,0 +1,374 @@
|
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Shared text helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/** Zero-width joiners, variation selectors, and the BOM — invisible, never meaningful in a cell. */
|
|
16
|
+
const INVISIBLE_PATTERN = /[-︎️⃣]/g;
|
|
17
|
+
/**
|
|
18
|
+
* Emoji, pictographs, and symbol runs. Deliberately Unicode-property based:
|
|
19
|
+
* a byte-range or "non-ASCII" filter would eat accented letters, and GTM data
|
|
20
|
+
* is full of Müller, Ørsted, and Škoda.
|
|
21
|
+
*/
|
|
22
|
+
const PICTOGRAPH_PATTERN = /[\p{Extended_Pictographic}\p{Emoji_Presentation}\p{Emoji_Modifier}\p{So}]/gu;
|
|
23
|
+
/** Leading/trailing punctuation, symbols, quotes, and whitespace. Inner punctuation is preserved. */
|
|
24
|
+
const OUTER_NOISE_PATTERN = /^[\s\p{P}\p{S}]+|[\s\p{P}\p{S}]+$/gu;
|
|
25
|
+
function toText(value) {
|
|
26
|
+
if (value === null || value === undefined)
|
|
27
|
+
return "";
|
|
28
|
+
if (typeof value === "string")
|
|
29
|
+
return value;
|
|
30
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
31
|
+
return String(value);
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(value) ?? "";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function collapseWhitespace(value) {
|
|
40
|
+
return value.replace(INVISIBLE_PATTERN, "").replace(/\s+/gu, " ").trim();
|
|
41
|
+
}
|
|
42
|
+
function trimOuterNoise(value) {
|
|
43
|
+
return value.replace(OUTER_NOISE_PATTERN, "");
|
|
44
|
+
}
|
|
45
|
+
/** True when every cased letter in the text is upper-case (digits/punctuation ignored). */
|
|
46
|
+
export function isAllCaps(value) {
|
|
47
|
+
let sawCasedLetter = false;
|
|
48
|
+
for (const char of value) {
|
|
49
|
+
if (!/\p{L}/u.test(char))
|
|
50
|
+
continue;
|
|
51
|
+
const lower = char.toLowerCase();
|
|
52
|
+
const upper = char.toUpperCase();
|
|
53
|
+
if (lower === upper)
|
|
54
|
+
continue; // caseless script (CJK, etc.) — not evidence either way
|
|
55
|
+
sawCasedLetter = true;
|
|
56
|
+
if (char !== upper)
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return sawCasedLetter;
|
|
60
|
+
}
|
|
61
|
+
/** Capitalizes the first letter of every word and lowercases the rest (Unicode-aware). */
|
|
62
|
+
export function titleCaseWords(value) {
|
|
63
|
+
return value.replace(/(\p{L})(\p{L}*)/gu, (_match, first, rest) => first.toUpperCase() + rest.toLowerCase());
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Company names
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Legal/corporate suffixes, stored as punctuation-free lower-case keys so
|
|
70
|
+
* "L.L.C.", "llc", and "LLC," all reduce to `llc`. Multi-word suffixes are
|
|
71
|
+
* joined ("pty ltd" → `ptyltd`) and matched against the trailing 1–3 tokens.
|
|
72
|
+
*/
|
|
73
|
+
export const COMPANY_LEGAL_SUFFIX_KEYS = new Set([
|
|
74
|
+
"inc", "incorporated", "corp", "corporation", "co", "company",
|
|
75
|
+
"ltd", "limited", "llc", "llp", "lp", "plc",
|
|
76
|
+
"gmbh", "ag", "sa", "sas", "sarl", "srl", "spa",
|
|
77
|
+
"bv", "nv", "oy", "ab", "as", "pty", "kg", "kk", "pte",
|
|
78
|
+
"sdnbhd", "ptyltd", "pteltd",
|
|
79
|
+
"ltda", "ug", "ev", "ou", "sro", "spzoo", "zrt", "kft", "doo",
|
|
80
|
+
"aps", "hf", "ehf", "ltee",
|
|
81
|
+
]);
|
|
82
|
+
/** The longest suffix key measured in whitespace-separated tokens ("sp. z o.o."). */
|
|
83
|
+
const MAX_SUFFIX_TOKENS = 3;
|
|
84
|
+
/** Separators after which a company string turns into a tagline, not a name. */
|
|
85
|
+
const SHORT_FORM_SEPARATORS = [" - ", " – ", " — ", " | ", ":", " / ", "("];
|
|
86
|
+
const SHORT_FORM_MAX_WORDS = 3;
|
|
87
|
+
function suffixKey(token) {
|
|
88
|
+
return token.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
89
|
+
}
|
|
90
|
+
function stripLegalSuffixes(tokens) {
|
|
91
|
+
let remaining = [...tokens];
|
|
92
|
+
// Bounded: every pass removes at least one token, so the token count is the budget.
|
|
93
|
+
for (let pass = 0; pass < tokens.length; pass += 1) {
|
|
94
|
+
while (remaining.length > 1 && suffixKey(remaining[remaining.length - 1] ?? "") === "") {
|
|
95
|
+
remaining = remaining.slice(0, -1);
|
|
96
|
+
}
|
|
97
|
+
let matched = false;
|
|
98
|
+
for (let span = Math.min(MAX_SUFFIX_TOKENS, remaining.length - 1); span >= 1; span -= 1) {
|
|
99
|
+
const key = remaining.slice(remaining.length - span).map(suffixKey).join("");
|
|
100
|
+
if (!key || !COMPANY_LEGAL_SUFFIX_KEYS.has(key))
|
|
101
|
+
continue;
|
|
102
|
+
remaining = remaining.slice(0, remaining.length - span);
|
|
103
|
+
matched = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (!matched)
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
return remaining;
|
|
110
|
+
}
|
|
111
|
+
function cutAtShortFormSeparator(value) {
|
|
112
|
+
let cut = value.length;
|
|
113
|
+
for (const separator of SHORT_FORM_SEPARATORS) {
|
|
114
|
+
const index = value.indexOf(separator);
|
|
115
|
+
if (index > 0 && index < cut)
|
|
116
|
+
cut = index;
|
|
117
|
+
}
|
|
118
|
+
return value.slice(0, cut);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Canonical company name: legal suffixes removed, stray punctuation and quotes
|
|
122
|
+
* trimmed, whitespace collapsed. Casing is the author's unless the whole name
|
|
123
|
+
* shouts ("ACME CORP." → "Acme"), because "hubspot" is how that company writes
|
|
124
|
+
* itself. `short` additionally cuts a tagline after a separator and keeps at
|
|
125
|
+
* most three words — the form that fits in an email's first sentence.
|
|
126
|
+
*
|
|
127
|
+
* Blank input returns null.
|
|
128
|
+
*/
|
|
129
|
+
export function normalizeCompanyName(value, mode = "display") {
|
|
130
|
+
const raw = collapseWhitespace(toText(value).replace(PICTOGRAPH_PATTERN, ""));
|
|
131
|
+
if (!raw)
|
|
132
|
+
return null;
|
|
133
|
+
const base = mode === "short" ? collapseWhitespace(cutAtShortFormSeparator(raw)) : raw;
|
|
134
|
+
if (!base)
|
|
135
|
+
return null;
|
|
136
|
+
const tokens = stripLegalSuffixes(base.split(" ").filter(Boolean));
|
|
137
|
+
let cleaned = collapseWhitespace(trimOuterNoise(tokens.join(" ")));
|
|
138
|
+
if (!cleaned)
|
|
139
|
+
cleaned = collapseWhitespace(trimOuterNoise(base));
|
|
140
|
+
if (!cleaned)
|
|
141
|
+
return null;
|
|
142
|
+
if (mode === "short") {
|
|
143
|
+
const words = cleaned.split(" ").filter(Boolean);
|
|
144
|
+
if (words.length > SHORT_FORM_MAX_WORDS) {
|
|
145
|
+
cleaned = words.slice(0, SHORT_FORM_MAX_WORDS).join(" ");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return isAllCaps(cleaned) ? titleCaseWords(cleaned) : cleaned;
|
|
149
|
+
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Job titles
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
/**
|
|
154
|
+
* Recruiting and self-promotion phrases that ride along in a scraped headline.
|
|
155
|
+
* Longest first so "we're hiring" is consumed before the bare "hiring".
|
|
156
|
+
*/
|
|
157
|
+
export const JOB_TITLE_SLOGANS = [
|
|
158
|
+
"we are hiring",
|
|
159
|
+
"we're hiring",
|
|
160
|
+
"join our team",
|
|
161
|
+
"open to work",
|
|
162
|
+
"let's connect",
|
|
163
|
+
"looking for",
|
|
164
|
+
"now hiring",
|
|
165
|
+
"opentowork",
|
|
166
|
+
"join us",
|
|
167
|
+
"dm me",
|
|
168
|
+
"hiring",
|
|
169
|
+
];
|
|
170
|
+
const SLOGAN_PATTERNS = JOB_TITLE_SLOGANS
|
|
171
|
+
.slice()
|
|
172
|
+
.sort((a, b) => b.length - a.length)
|
|
173
|
+
.map((phrase) => {
|
|
174
|
+
const body = phrase
|
|
175
|
+
.split("")
|
|
176
|
+
.map((char) => {
|
|
177
|
+
if (char === " ")
|
|
178
|
+
return "\\s+";
|
|
179
|
+
if (char === "'")
|
|
180
|
+
return "['’‘ʼ´]";
|
|
181
|
+
return char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
182
|
+
})
|
|
183
|
+
.join("");
|
|
184
|
+
// No lookbehind: Safari shipped it late and this engine runs in the browser.
|
|
185
|
+
return new RegExp(`(^|[^\\p{L}\\p{N}])(?:${body})(?![\\p{L}\\p{N}])`, "giu");
|
|
186
|
+
});
|
|
187
|
+
/**
|
|
188
|
+
* Separators that join a real title to a second clause. A bare `-`, `,`, `/`,
|
|
189
|
+
* or `&` is NOT one of them: "Account Executive - EMEA", "Sr. Director, Demand
|
|
190
|
+
* Generation", and "Director of Importing/Exporting" are whole titles.
|
|
191
|
+
*/
|
|
192
|
+
const JOB_TITLE_SEPARATOR_PATTERN = /\s*\|\s*|\s*[•·]\s*|\s*::\s*|\s+[—–]\s+/u;
|
|
193
|
+
/**
|
|
194
|
+
* Canonical job title: emoji, hashtags, parentheticals, and hiring slogans
|
|
195
|
+
* removed, then only the first clause of a separator-joined headline kept.
|
|
196
|
+
* All-caps titles are title-cased; every other casing is the author's.
|
|
197
|
+
*
|
|
198
|
+
* Blank input, or input that was nothing but noise, returns null.
|
|
199
|
+
*/
|
|
200
|
+
export function cleanJobTitle(value) {
|
|
201
|
+
let text = toText(value);
|
|
202
|
+
if (!text.trim())
|
|
203
|
+
return null;
|
|
204
|
+
text = text.replace(PICTOGRAPH_PATTERN, "").replace(INVISIBLE_PATTERN, "");
|
|
205
|
+
// Parentheticals, innermost-out; bounded so a pathological paste cannot spin.
|
|
206
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
207
|
+
const next = text.replace(/\([^()]*\)/g, " ").replace(/\[[^[\]]*\]/g, " ");
|
|
208
|
+
if (next === text)
|
|
209
|
+
break;
|
|
210
|
+
text = next;
|
|
211
|
+
}
|
|
212
|
+
text = text.replace(/#[\p{L}\p{N}_-]+/gu, " ");
|
|
213
|
+
for (const pattern of SLOGAN_PATTERNS)
|
|
214
|
+
text = text.replace(pattern, "$1");
|
|
215
|
+
text = collapseWhitespace(text);
|
|
216
|
+
const segments = text.split(JOB_TITLE_SEPARATOR_PATTERN).map((part) => (part ?? "").trim());
|
|
217
|
+
const chosen = segments.find((part) => /[\p{L}\p{N}]/u.test(part)) ?? segments[0] ?? "";
|
|
218
|
+
const cleaned = collapseWhitespace(trimOuterNoise(chosen));
|
|
219
|
+
if (!cleaned)
|
|
220
|
+
return null;
|
|
221
|
+
return isAllCaps(cleaned) ? titleCaseWords(cleaned) : cleaned;
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Delimited lists
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
/** Tried in order when no separator is given; the first one present wins. */
|
|
227
|
+
const LIST_DELIMITERS = ["\n", ";", "|", ","];
|
|
228
|
+
function cleanListParts(parts) {
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const part of parts) {
|
|
232
|
+
const text = collapseWhitespace(toText(part));
|
|
233
|
+
if (!text || seen.has(text))
|
|
234
|
+
continue;
|
|
235
|
+
seen.add(text);
|
|
236
|
+
out.push(text);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Turns one messy cell into a clean array: trimmed, blank-free, de-duplicated
|
|
242
|
+
* in first-seen order. An existing array or a JSON-encoded array is cleaned in
|
|
243
|
+
* place; a string is split on the given separator, or on the first delimiter
|
|
244
|
+
* actually present (newline, `;`, `|`, `,`) when none is given.
|
|
245
|
+
*
|
|
246
|
+
* Blank input returns an empty array.
|
|
247
|
+
*/
|
|
248
|
+
export function splitDelimitedList(value, separator) {
|
|
249
|
+
if (Array.isArray(value))
|
|
250
|
+
return cleanListParts(value);
|
|
251
|
+
if (value === null || value === undefined)
|
|
252
|
+
return [];
|
|
253
|
+
const text = typeof value === "string" ? value : toText(value);
|
|
254
|
+
const trimmed = text.trim();
|
|
255
|
+
if (!trimmed)
|
|
256
|
+
return [];
|
|
257
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(trimmed);
|
|
260
|
+
if (Array.isArray(parsed))
|
|
261
|
+
return cleanListParts(parsed);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Not JSON after all — fall through to delimiter splitting.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const explicit = typeof separator === "string" ? separator : "";
|
|
268
|
+
if (explicit)
|
|
269
|
+
return cleanListParts(trimmed.split(explicit));
|
|
270
|
+
if (/\r?\n/.test(trimmed))
|
|
271
|
+
return cleanListParts(trimmed.split(/\r?\n/));
|
|
272
|
+
for (const delimiter of LIST_DELIMITERS) {
|
|
273
|
+
if (delimiter !== "\n" && trimmed.includes(delimiter)) {
|
|
274
|
+
return cleanListParts(trimmed.split(delimiter));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return cleanListParts([trimmed]);
|
|
278
|
+
}
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Email classification
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
/** Consumer mailbox providers — a person, not a company domain. */
|
|
283
|
+
export const FREE_MAIL_DOMAINS = new Set([
|
|
284
|
+
"gmail.com", "googlemail.com",
|
|
285
|
+
"yahoo.com", "yahoo.co.uk", "yahoo.fr", "yahoo.de", "yahoo.co.jp", "yahoo.ca",
|
|
286
|
+
"yahoo.com.au", "yahoo.es", "yahoo.it", "ymail.com", "rocketmail.com",
|
|
287
|
+
"hotmail.com", "hotmail.co.uk", "hotmail.fr", "hotmail.de", "hotmail.it", "hotmail.es",
|
|
288
|
+
"outlook.com", "outlook.fr", "outlook.de", "outlook.es", "outlook.co.uk",
|
|
289
|
+
"live.com", "live.co.uk", "live.fr", "live.de", "msn.com",
|
|
290
|
+
"aol.com", "icloud.com", "me.com", "mac.com",
|
|
291
|
+
"protonmail.com", "proton.me", "pm.me",
|
|
292
|
+
"gmx.com", "gmx.de", "gmx.net", "gmx.at", "gmx.ch",
|
|
293
|
+
"web.de", "t-online.de", "freenet.de",
|
|
294
|
+
"mail.com", "mail.ru", "yandex.ru", "yandex.com",
|
|
295
|
+
"zoho.com", "fastmail.com", "hey.com", "tutanota.com", "tuta.io",
|
|
296
|
+
"orange.fr", "wanadoo.fr", "free.fr", "laposte.net", "sfr.fr",
|
|
297
|
+
"libero.it", "virgilio.it", "seznam.cz",
|
|
298
|
+
"wp.pl", "o2.pl", "onet.pl", "interia.pl",
|
|
299
|
+
"qq.com", "163.com", "126.com", "sina.com",
|
|
300
|
+
"naver.com", "daum.net", "hanmail.net", "rediffmail.com",
|
|
301
|
+
"bigpond.com", "optusnet.com.au", "telus.net", "shaw.ca", "sympatico.ca", "rogers.com",
|
|
302
|
+
"btinternet.com", "sky.com", "talktalk.net", "virginmedia.com", "blueyonder.co.uk",
|
|
303
|
+
"comcast.net", "verizon.net", "att.net", "sbcglobal.net", "cox.net",
|
|
304
|
+
"charter.net", "earthlink.net", "bellsouth.net",
|
|
305
|
+
"ziggo.nl", "kpnmail.nl", "home.nl", "telenet.be", "skynet.be", "bluewin.ch",
|
|
306
|
+
"uol.com.br", "bol.com.br", "terra.com.br",
|
|
307
|
+
]);
|
|
308
|
+
/** Throwaway inbox providers — never worth a send, never worth a credit. */
|
|
309
|
+
export const DISPOSABLE_EMAIL_DOMAINS = new Set([
|
|
310
|
+
"mailinator.com", "guerrillamail.com", "guerrillamail.net",
|
|
311
|
+
"10minutemail.com", "10minutemail.net",
|
|
312
|
+
"tempmail.com", "temp-mail.org", "temp-mail.io", "tempr.email",
|
|
313
|
+
"yopmail.com", "yopmail.fr", "trashmail.com", "trashmail.me",
|
|
314
|
+
"getnada.com", "dispostable.com", "maildrop.cc", "sharklasers.com",
|
|
315
|
+
"throwawaymail.com", "fakeinbox.com", "mohmal.com", "mailnesia.com",
|
|
316
|
+
"spamgourmet.com", "discard.email", "mintemail.com", "emailondeck.com",
|
|
317
|
+
"burnermail.io", "tempail.com", "mytemp.email", "moakt.com",
|
|
318
|
+
"tmpmail.org", "tmpmail.net", "minutemail.com", "mailcatch.com",
|
|
319
|
+
"spam4.me", "harakirimail.com", "jetable.org", "mailexpire.com",
|
|
320
|
+
"throwam.com", "tempinbox.com", "inboxkitten.com",
|
|
321
|
+
]);
|
|
322
|
+
/** Shared-inbox local parts — a department, not a person. */
|
|
323
|
+
export const ROLE_EMAIL_LOCAL_PARTS = new Set([
|
|
324
|
+
"info", "sales", "support", "help", "admin", "administrator",
|
|
325
|
+
"contact", "hello", "hi", "team", "office",
|
|
326
|
+
"billing", "invoices", "accounts", "accounting", "finance",
|
|
327
|
+
"marketing", "hr", "jobs", "careers", "recruiting",
|
|
328
|
+
"press", "media", "pr", "legal", "privacy", "security", "abuse",
|
|
329
|
+
"postmaster", "webmaster", "hostmaster",
|
|
330
|
+
"noreply", "no-reply", "donotreply", "do-not-reply",
|
|
331
|
+
"newsletter", "notifications", "notification",
|
|
332
|
+
"mail", "mailer-daemon", "service", "services",
|
|
333
|
+
"customerservice", "customer-service",
|
|
334
|
+
"enquiries", "inquiries", "enquiry", "inquiry",
|
|
335
|
+
"orders", "order", "partners", "partnerships",
|
|
336
|
+
"feedback", "welcome", "community",
|
|
337
|
+
"dev", "devops", "it", "ops", "engineering",
|
|
338
|
+
"alerts", "subscriptions",
|
|
339
|
+
]);
|
|
340
|
+
const EMAIL_SYNTAX_PATTERN = /^[^\s@,;:<>"'()[\]\\]+@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
341
|
+
/** A domain matches a list when it IS a listed domain or a subdomain of one (mail.yahoo.com). */
|
|
342
|
+
function matchesDomainList(domain, list) {
|
|
343
|
+
if (list.has(domain))
|
|
344
|
+
return true;
|
|
345
|
+
const labels = domain.split(".");
|
|
346
|
+
for (let index = 1; index < labels.length - 1; index += 1) {
|
|
347
|
+
if (list.has(labels.slice(index).join(".")))
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Classifies an email address from OXYGEN's built-in lists, in precedence
|
|
354
|
+
* order: a throwaway domain wins, then a shared-inbox local part, then a
|
|
355
|
+
* consumer mailbox provider, and anything left is a company address.
|
|
356
|
+
*
|
|
357
|
+
* Blank or syntactically invalid input returns null.
|
|
358
|
+
*/
|
|
359
|
+
export function classifyEmailType(value) {
|
|
360
|
+
const email = toText(value).trim().toLowerCase();
|
|
361
|
+
if (!email || !EMAIL_SYNTAX_PATTERN.test(email))
|
|
362
|
+
return null;
|
|
363
|
+
const at = email.lastIndexOf("@");
|
|
364
|
+
const localPart = email.slice(0, at);
|
|
365
|
+
const domain = email.slice(at + 1);
|
|
366
|
+
if (matchesDomainList(domain, DISPOSABLE_EMAIL_DOMAINS))
|
|
367
|
+
return "disposable";
|
|
368
|
+
const untagged = localPart.split("+")[0] ?? localPart;
|
|
369
|
+
if (ROLE_EMAIL_LOCAL_PARTS.has(untagged) || ROLE_EMAIL_LOCAL_PARTS.has(localPart))
|
|
370
|
+
return "role";
|
|
371
|
+
if (matchesDomainList(domain, FREE_MAIL_DOMAINS))
|
|
372
|
+
return "personal";
|
|
373
|
+
return "work";
|
|
374
|
+
}
|
|
@@ -7,6 +7,8 @@ export type RecipeToolRunOptions = {
|
|
|
7
7
|
optional?: boolean;
|
|
8
8
|
/** Pin this call to one provider account for every replay/scheduled run. */
|
|
9
9
|
connectionId?: string;
|
|
10
|
+
/** Pin the credential rail instead of automatically selecting a connected key. */
|
|
11
|
+
credentialMode?: "managed" | "user_api_key" | "user_oauth";
|
|
10
12
|
};
|
|
11
13
|
export type RecipeToolApi = {
|
|
12
14
|
run: <T = unknown>(toolId: string, payload?: Record<string, unknown>, options?: RecipeToolRunOptions) => Promise<T>;
|
|
@@ -172,36 +172,43 @@ export declare const WALKTHROUGH_COMPLETION_BONUS_CREDITS = 1000;
|
|
|
172
172
|
export declare const WALKTHROUGH_SCRAPE_MAX_CREDITS = 2000;
|
|
173
173
|
export declare const BASE_PRICING_PLANS: {
|
|
174
174
|
readonly free: {
|
|
175
|
+
readonly monthlyPriceCents: number | null;
|
|
176
|
+
readonly monthlyCredits: number | null;
|
|
177
|
+
readonly weeklyCreditsLimit: number | null;
|
|
178
|
+
readonly rolloverCap: number | null;
|
|
179
|
+
readonly byokEnabled: boolean;
|
|
180
|
+
readonly tier: "free";
|
|
181
|
+
readonly name: "Free";
|
|
175
182
|
readonly monthlyAutomationActions: null;
|
|
176
183
|
readonly automationOverageCentsPerMillion: null;
|
|
177
184
|
readonly automationOverageEnabledDefault: false;
|
|
178
185
|
readonly description: "Your CRM, tables, and workflows — free forever. Credits pay for enrichment and AI; connecting a sender, publishing, managed email infrastructure, and your own provider keys need a plan.";
|
|
179
186
|
readonly ctaLabel: "Upgrade";
|
|
180
187
|
readonly features: readonly ["CRM, Tables, Workflows, and Knowledge — unmetered", "Sequences: author, preview, and enroll", "Agents and Workspace Copilot", "Credits for enrichment and AI, topped up any time"];
|
|
188
|
+
};
|
|
189
|
+
readonly starter: {
|
|
181
190
|
readonly monthlyPriceCents: number | null;
|
|
182
191
|
readonly monthlyCredits: number | null;
|
|
183
192
|
readonly weeklyCreditsLimit: number | null;
|
|
184
193
|
readonly rolloverCap: number | null;
|
|
185
194
|
readonly byokEnabled: boolean;
|
|
186
|
-
readonly tier: "
|
|
187
|
-
readonly name: "
|
|
188
|
-
};
|
|
189
|
-
readonly starter: {
|
|
195
|
+
readonly tier: "starter";
|
|
196
|
+
readonly name: "Starter";
|
|
190
197
|
readonly monthlyAutomationActions: null;
|
|
191
198
|
readonly automationOverageCentsPerMillion: null;
|
|
192
199
|
readonly automationOverageEnabledDefault: false;
|
|
193
200
|
readonly description: "For founders running focused managed enrichment and AI columns.";
|
|
194
201
|
readonly ctaLabel: "Choose Starter";
|
|
195
202
|
readonly features: readonly ["99,000 credits every month", "All integrations", "Workflows", "Bring your own keys", "Standard support"];
|
|
203
|
+
};
|
|
204
|
+
readonly pro: {
|
|
196
205
|
readonly monthlyPriceCents: number | null;
|
|
197
206
|
readonly monthlyCredits: number | null;
|
|
198
207
|
readonly weeklyCreditsLimit: number | null;
|
|
199
208
|
readonly rolloverCap: number | null;
|
|
200
209
|
readonly byokEnabled: boolean;
|
|
201
|
-
readonly tier: "
|
|
202
|
-
readonly name: "
|
|
203
|
-
};
|
|
204
|
-
readonly pro: {
|
|
210
|
+
readonly tier: "pro";
|
|
211
|
+
readonly name: "Pro";
|
|
205
212
|
readonly monthlyAutomationActions: null;
|
|
206
213
|
readonly automationOverageCentsPerMillion: null;
|
|
207
214
|
readonly automationOverageEnabledDefault: false;
|
|
@@ -210,30 +217,30 @@ export declare const BASE_PRICING_PLANS: {
|
|
|
210
217
|
readonly highlighted: true;
|
|
211
218
|
readonly ctaLabel: "Choose Pro";
|
|
212
219
|
readonly features: readonly ["249,000 credits every month", "All integrations", "Workflows", "Bring your own keys", "Standard support"];
|
|
220
|
+
};
|
|
221
|
+
readonly team: {
|
|
213
222
|
readonly monthlyPriceCents: number | null;
|
|
214
223
|
readonly monthlyCredits: number | null;
|
|
215
224
|
readonly weeklyCreditsLimit: number | null;
|
|
216
225
|
readonly rolloverCap: number | null;
|
|
217
226
|
readonly byokEnabled: boolean;
|
|
218
|
-
readonly tier: "
|
|
219
|
-
readonly name: "
|
|
220
|
-
};
|
|
221
|
-
readonly team: {
|
|
227
|
+
readonly tier: "team";
|
|
228
|
+
readonly name: "Team";
|
|
222
229
|
readonly monthlyAutomationActions: null;
|
|
223
230
|
readonly automationOverageCentsPerMillion: null;
|
|
224
231
|
readonly automationOverageEnabledDefault: false;
|
|
225
232
|
readonly description: "For small GTM teams and agencies running shared workflows with their own AI keys.";
|
|
226
233
|
readonly ctaLabel: "Choose Team";
|
|
227
234
|
readonly features: readonly ["749,000 credits every month", "All integrations", "Workflows", "Bring your own keys", "Premium support"];
|
|
235
|
+
};
|
|
236
|
+
readonly scale: {
|
|
228
237
|
readonly monthlyPriceCents: number | null;
|
|
229
238
|
readonly monthlyCredits: number | null;
|
|
230
239
|
readonly weeklyCreditsLimit: number | null;
|
|
231
240
|
readonly rolloverCap: number | null;
|
|
232
241
|
readonly byokEnabled: boolean;
|
|
233
|
-
readonly tier: "
|
|
234
|
-
readonly name: "
|
|
235
|
-
};
|
|
236
|
-
readonly scale: {
|
|
242
|
+
readonly tier: "scale";
|
|
243
|
+
readonly name: "Scale";
|
|
237
244
|
readonly monthlyAutomationActions: null;
|
|
238
245
|
readonly automationOverageCentsPerMillion: null;
|
|
239
246
|
readonly automationOverageEnabledDefault: false;
|
|
@@ -241,15 +248,15 @@ export declare const BASE_PRICING_PLANS: {
|
|
|
241
248
|
readonly ctaLabel: "Contact sales";
|
|
242
249
|
readonly contactHref: "mailto:priority@oxygen-agent.com";
|
|
243
250
|
readonly features: readonly ["10,000,000+ managed credits / month", "Workflows", "Phone number enrichment on managed credits", "Custom provider routing and BYOK policies", "Dedicated onboarding"];
|
|
251
|
+
};
|
|
252
|
+
readonly enterprise: {
|
|
244
253
|
readonly monthlyPriceCents: number | null;
|
|
245
254
|
readonly monthlyCredits: number | null;
|
|
246
255
|
readonly weeklyCreditsLimit: number | null;
|
|
247
256
|
readonly rolloverCap: number | null;
|
|
248
257
|
readonly byokEnabled: boolean;
|
|
249
|
-
readonly tier: "
|
|
250
|
-
readonly name: "
|
|
251
|
-
};
|
|
252
|
-
readonly enterprise: {
|
|
258
|
+
readonly tier: "enterprise";
|
|
259
|
+
readonly name: "Enterprise";
|
|
253
260
|
readonly monthlyAutomationActions: null;
|
|
254
261
|
readonly automationOverageCentsPerMillion: null;
|
|
255
262
|
readonly automationOverageEnabledDefault: false;
|
|
@@ -257,13 +264,6 @@ export declare const BASE_PRICING_PLANS: {
|
|
|
257
264
|
readonly ctaLabel: "Talk to us";
|
|
258
265
|
readonly contactHref: "mailto:priority@oxygen-agent.com";
|
|
259
266
|
readonly features: readonly ["Everything in Team", "Custom usage limits", "Volume pricing", "Dedicated support"];
|
|
260
|
-
readonly monthlyPriceCents: number | null;
|
|
261
|
-
readonly monthlyCredits: number | null;
|
|
262
|
-
readonly weeklyCreditsLimit: number | null;
|
|
263
|
-
readonly rolloverCap: number | null;
|
|
264
|
-
readonly byokEnabled: boolean;
|
|
265
|
-
readonly tier: "enterprise";
|
|
266
|
-
readonly name: "Enterprise";
|
|
267
267
|
};
|
|
268
268
|
};
|
|
269
269
|
export declare const PUBLIC_PLAN_ORDER: readonly PlanTier[];
|
|
@@ -23,6 +23,12 @@ export type CapabilityRouteMatch = {
|
|
|
23
23
|
card: CapabilityRouteCard;
|
|
24
24
|
recommendedTools: string[];
|
|
25
25
|
recommendedCommands: string[];
|
|
26
|
+
/**
|
|
27
|
+
* The one exact invocation for asks whose answer is a single command with a
|
|
28
|
+
* specific flag the help text alone did not teach. Present only when the
|
|
29
|
+
* intent is that narrow; `recommendedCommands[0]` is the same command.
|
|
30
|
+
*/
|
|
31
|
+
exactCommand?: string;
|
|
26
32
|
};
|
|
27
33
|
export type PrimitiveRouteMatch = CapabilityRouteMatch & {
|
|
28
34
|
card: PrimitiveRouteCard;
|
|
@@ -34,3 +40,5 @@ export declare function getCapabilityRouteMatch(id: string): CapabilityRouteMatc
|
|
|
34
40
|
export declare function inferCapabilityRoute(query: string): CapabilityRouteMatch | null;
|
|
35
41
|
export declare function inferPrimitiveRoute(query: string): PrimitiveRouteMatch | null;
|
|
36
42
|
export declare function serializeCapabilityRoute(route: CapabilityRouteMatch | null): Record<string, unknown> | null;
|
|
43
|
+
/** The exact `companies search plan` invocation for the url_recovery intent. */
|
|
44
|
+
export declare const COMPANY_URL_RECOVERY_COMMAND = "oxygen companies search plan --prompt \"Find the websites for these companies: <company names>\" --source-intent url_recovery";
|