@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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/functions-commands.js +21 -5
  3. package/dist/help.d.ts +21 -0
  4. package/dist/help.js +93 -0
  5. package/dist/index.js +322 -57
  6. package/dist/ugc-commands.d.ts +3 -6
  7. package/dist/ugc-commands.js +2 -1200
  8. package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
  9. package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
  10. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
  11. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
  12. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
  13. package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
  14. package/node_modules/@oxygen/cli-ugc/package.json +15 -0
  15. package/node_modules/@oxygen/formula/dist/expression.js +14 -1
  16. package/node_modules/@oxygen/formula/dist/formula-functions.js +71 -1
  17. package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
  18. package/node_modules/@oxygen/formula/dist/index.js +1 -0
  19. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +69 -0
  20. package/node_modules/@oxygen/formula/dist/value-cleaners.js +374 -0
  21. package/node_modules/@oxygen/shared/dist/billing.d.ts +27 -27
  22. package/node_modules/@oxygen/shared/dist/capability-discovery.js +29 -4
  23. package/node_modules/@oxygen/shared/dist/column-output-fields.js +12 -4
  24. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +18 -0
  25. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
  26. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
  27. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
  29. package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
  30. package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
  31. package/node_modules/@oxygen/shared/dist/index.js +1 -0
  32. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +15 -0
  33. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +26 -4
  34. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +12 -1
  35. package/node_modules/@oxygen/shared/dist/langfuse.js +48 -8
  36. package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +33 -1
  37. package/node_modules/@oxygen/shared/dist/research-output-contract.js +64 -2
  38. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
  39. package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/sequences.d.ts +26 -0
  41. package/node_modules/@oxygen/shared/dist/sequences.js +24 -0
  42. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  43. package/node_modules/@oxygen/shared/package.json +10 -0
  44. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
  45. 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: "free";
187
- readonly name: "Free";
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: "starter";
202
- readonly name: "Starter";
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: "pro";
219
- readonly name: "Pro";
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: "team";
234
- readonly name: "Team";
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: "scale";
250
- readonly name: "Scale";
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
- 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"],
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?.evidenceMode === "strict",
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
- : (isRecord(definition.outputSchema) ? definition.outputSchema : null);
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;