@oxygen-agent/cli 1.982.3 → 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/functions-commands.js +21 -5
- package/dist/help.d.ts +21 -0
- package/dist/help.js +93 -0
- package/dist/index.js +322 -57
- package/dist/ugc-commands.d.ts +3 -6
- package/dist/ugc-commands.js +2 -1200
- 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/shared/dist/billing.d.ts +27 -27
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +29 -4
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +12 -4
- 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/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +26 -4
- 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/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 +26 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +24 -0
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +10 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
- package/package.json +5 -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
|
+
}
|
|
@@ -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[];
|
|
@@ -210,10 +210,10 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
210
210
|
primitive: "tables",
|
|
211
211
|
owns: "Typed working datasets, rows, formulas, AI/tool/waterfall columns, reusable Functions with isolated drafts and published versions, cell state, projects, and run provenance.",
|
|
212
212
|
notFor: "Canonical CRM truth, message cadence, or an off-platform spreadsheet runtime.",
|
|
213
|
-
execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. For standard person or company enrichment, `columns add <table> --preset person_enrich|company_enrich` (MCP oxygen_columns_add with preset) adds the maintained bundle in one call before any hand-built tool column.",
|
|
213
|
+
execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. For standard person or company enrichment, `columns add <table> --preset person_enrich|company_enrich` (MCP oxygen_columns_add with preset) adds the maintained bundle in one call before any hand-built tool column. Person questions over that bundle's profile payload (skill set, grad school, location, job fit, current company) and person appearance research (events, keynotes, podcasts, GitHub profile) are `columns catalog --category people` templates, added with `columns add <table> --prompt-key <key> --input <name>=<column>`. For a company fact OXYGEN already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, LinkedIn/Crunchbase page lookups) or a page extraction, `columns catalog` lists the ready-made template and `columns add <table> --prompt-key <key> --input <name>=<column>` (MCP oxygen_columns_add with prompt_key) adds it with no prompt to write.",
|
|
214
214
|
posture: "mixed",
|
|
215
|
-
gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
|
|
216
|
-
gatewayCommands: ["tables create", "columns add", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
215
|
+
gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
|
|
216
|
+
gatewayCommands: ["tables create", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
217
217
|
skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-linkedin-marketing"],
|
|
218
218
|
endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "supabase", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
|
|
219
219
|
// "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
|
|
@@ -221,7 +221,12 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
221
221
|
// `tables create` / `columns add` / `enrich-column preview` — none of which
|
|
222
222
|
// do it. The agent only found the right command by grepping the raw 25k-line
|
|
223
223
|
// command manifest, which is not a discovery path a customer has.
|
|
224
|
-
|
|
224
|
+
// "founders" / "parent company" / "cloud provider" / "company research" route
|
|
225
|
+
// here for `columns catalog`. Added after the 2026-09-16 blind baseline asked
|
|
226
|
+
// `capabilities search "founders of a company"` and got `route: null` with a
|
|
227
|
+
// generic sourcing pointer, then hand-wrote four research prompts for
|
|
228
|
+
// questions the template catalog now answers by key.
|
|
229
|
+
intentTerms: ["table", "spreadsheet", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable", "column template", "skill set", "grad school", "podcast appearance", "keynote", "github profile", "job fit", "founders", "parent company", "subsidiaries", "cloud provider", "company research", "research question", "research column", "web research", "template catalog", "ready-made", "offers demos", "naics"],
|
|
225
230
|
},
|
|
226
231
|
{
|
|
227
232
|
id: "messages",
|
|
@@ -838,6 +843,16 @@ function recommendationsFor(card, query) {
|
|
|
838
843
|
commands: ["tables watcher preview", "tables watcher create", "tables watcher get", "tables watcher update", "tables watcher pause", "tables watcher resume"],
|
|
839
844
|
};
|
|
840
845
|
}
|
|
846
|
+
if (isCompanyResearchQuestionIntent(query)) {
|
|
847
|
+
// A fact the template catalog already answers by key: list the keys first
|
|
848
|
+
// (free), add the column by key (free), then price and run it. Ahead of
|
|
849
|
+
// the bare "run" noun below, because "which cloud provider does it run
|
|
850
|
+
// on" is a question about the company, not about a run.
|
|
851
|
+
return {
|
|
852
|
+
tools: ["oxygen_columns_catalog", "oxygen_columns_add", "oxygen_columns_run", "oxygen_tables_describe"],
|
|
853
|
+
commands: ["columns catalog", "columns add", "columns run", "tables describe"],
|
|
854
|
+
};
|
|
855
|
+
}
|
|
841
856
|
if (/\b(table )?(action )?runs?\b/.test(query)) {
|
|
842
857
|
return {
|
|
843
858
|
tools: ["oxygen_table_runs_get", "oxygen_table_runs_items", "oxygen_table_runs_wait", "oxygen_table_runs_retry_failed"],
|
|
@@ -1012,6 +1027,16 @@ function isEmailVerificationIntent(query) {
|
|
|
1012
1027
|
// ask ("find companies with a website in Germany") keeps its owner. LinkedIn
|
|
1013
1028
|
// page recovery is deliberately left to the LinkedIn rules above it, which
|
|
1014
1029
|
// already resolve company pages through the public-research catalog.
|
|
1030
|
+
/**
|
|
1031
|
+
* A company fact the template catalog answers by key — founders, parent company,
|
|
1032
|
+
* subsidiaries, cloud provider, NAICS, demos — and the "ready-made / template
|
|
1033
|
+
* catalog / research question" phrasing itself. Kept to nouns no other card
|
|
1034
|
+
* owns: "funding" alone belongs to Signals and "industry" alone to company
|
|
1035
|
+
* search, so both stay off this list.
|
|
1036
|
+
*/
|
|
1037
|
+
function isCompanyResearchQuestionIntent(query) {
|
|
1038
|
+
return /\b(founders?|founded by|parent compan(?:y|ies)|subsidiar(?:y|ies)|cloud provider|naics|column templates?|template catalog|ready[- ]made|research question|company research|offers? demos?|gives? demos?)\b/.test(query);
|
|
1039
|
+
}
|
|
1015
1040
|
function isCompanyUrlRecoveryIntent(query) {
|
|
1016
1041
|
const urlNoun = /\b(websites?|web ?sites?|website urls?|domains?|urls?|homepages?)\b/;
|
|
1017
1042
|
const companyNoun = /\b(compan(?:y|ies)|accounts?|organi[sz]ations?|brands?|vendors?)\b/;
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* may contain a dot or a space. Such a field carries `referenceable: false` and
|
|
27
27
|
* is display-only.
|
|
28
28
|
*/
|
|
29
|
-
import { buildResearchCellSchema, deriveResearchOutputContract, usesServerManagedResearchSchema, } from "./research-output-contract.js";
|
|
29
|
+
import { buildResearchCellSchema, isResearchEnvelopeSchema, researchEvidenceRequired, deriveResearchOutputContract, usesServerManagedResearchSchema, } from "./research-output-contract.js";
|
|
30
30
|
import { isTemplateSafePath, parseJsonPath, readJsonPath } from "./json-path.js";
|
|
31
31
|
const MAX_FIELDS = 40;
|
|
32
32
|
const MAX_SCHEMA_DEPTH = 5;
|
|
@@ -263,13 +263,21 @@ function researchContract(definition) {
|
|
|
263
263
|
const prompt = readString(definition.prompt) ?? "";
|
|
264
264
|
const webSearch = isRecord(definition.webSearch) ? definition.webSearch : null;
|
|
265
265
|
const contract = deriveResearchOutputContract(prompt, {
|
|
266
|
-
evidenceRequired: webSearch
|
|
266
|
+
evidenceRequired: researchEvidenceRequired(webSearch),
|
|
267
267
|
});
|
|
268
268
|
// The STORED shape, not the model-facing one: a research cell carries
|
|
269
|
-
// `sources`, never the `citations` the model answered with.
|
|
269
|
+
// `sources`, never the `citations` the model answered with. A column whose
|
|
270
|
+
// own schema is still the research envelope (a catalog template's typed
|
|
271
|
+
// answer) gets the same rewrite, so `answer.plan_count` is offered and
|
|
272
|
+
// `citations` is not; a hand-authored schema that replaced the envelope is
|
|
273
|
+
// used verbatim, because the cell then holds exactly what it declares.
|
|
270
274
|
const schema = serverManaged
|
|
271
275
|
? buildResearchCellSchema(contract)
|
|
272
|
-
:
|
|
276
|
+
: isRecord(definition.outputSchema)
|
|
277
|
+
? (isResearchEnvelopeSchema(definition.outputSchema)
|
|
278
|
+
? buildResearchCellSchema(null, definition.outputSchema)
|
|
279
|
+
: definition.outputSchema)
|
|
280
|
+
: null;
|
|
273
281
|
if (!schema)
|
|
274
282
|
return EMPTY_CONTRACT;
|
|
275
283
|
return {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type CopilotPlaybookSlug = "tam-sourcing" | "linkedin-content-strategy" | "inbound-led-outbound" | "signal-based-outbound";
|
|
2
|
+
export type CopilotPlaybook = {
|
|
3
|
+
/**
|
|
4
|
+
* The skill slug the Copilot loads through its `skills` tool, and the
|
|
5
|
+
* `name:` in the playbook file's frontmatter. The two are gated equal.
|
|
6
|
+
*/
|
|
7
|
+
slug: CopilotPlaybookSlug;
|
|
8
|
+
/** Customer-facing name, exactly as it is spoken in the composer prompt and shown on the Home chip. */
|
|
9
|
+
title: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const COPILOT_PLAYBOOKS: readonly CopilotPlaybook[];
|
|
12
|
+
/**
|
|
13
|
+
* The sentence a Home card writes into the composer. It names the skill the way
|
|
14
|
+
* a person would say it, and the Copilot's prompt maps that title back to the
|
|
15
|
+
* slug — so the user reads and sends exactly what the card promised.
|
|
16
|
+
*/
|
|
17
|
+
export declare function copilotPlaybookPrompt(playbook: Pick<CopilotPlaybook, "title">): string;
|
|
18
|
+
export declare function getCopilotPlaybook(slug: string): CopilotPlaybook | null;
|