@oxygen-agent/cli 1.1011.1091 → 1.1013.15

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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.d.ts +3 -1
  3. package/dist/command-manifest.js +11 -0
  4. package/dist/customer-operation-commands.js +7 -3
  5. package/dist/diagnostic-upload-main.d.ts +1 -0
  6. package/dist/diagnostic-upload-main.js +2 -0
  7. package/dist/diagnostic-uploader.d.ts +3 -0
  8. package/dist/diagnostic-uploader.js +68 -0
  9. package/dist/diagnostics.d.ts +22 -0
  10. package/dist/diagnostics.js +219 -0
  11. package/dist/http-client.d.ts +7 -0
  12. package/dist/http-client.js +12 -2
  13. package/dist/index.js +72 -34
  14. package/dist/transcript.js +1 -1
  15. package/node_modules/@oxygen/formula/dist/expression.js +16 -0
  16. package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +8 -0
  17. package/node_modules/@oxygen/shared/dist/attempt-provenance.js +15 -0
  18. package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
  19. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +2 -1
  20. package/node_modules/@oxygen/shared/dist/capability-discovery.js +28 -4
  21. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +3 -3
  22. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +3 -3
  23. package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/geo.js +160 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +1 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  27. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +10 -4
  29. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
  30. package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
  31. package/node_modules/@oxygen/shared/dist/diagnostic-context.d.ts +3 -0
  32. package/node_modules/@oxygen/shared/dist/diagnostic-context.js +17 -0
  33. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +4 -0
  34. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +12 -0
  35. package/node_modules/@oxygen/shared/dist/diagnostic-lock.d.ts +2 -0
  36. package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +60 -0
  37. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.d.ts +6 -0
  38. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.js +22 -0
  39. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.d.ts +2 -0
  40. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +39 -0
  41. package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +31 -0
  42. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +233 -0
  43. package/node_modules/@oxygen/shared/dist/diagnostics.d.ts +49 -0
  44. package/node_modules/@oxygen/shared/dist/diagnostics.js +253 -0
  45. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
  46. package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
  47. package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
  48. package/node_modules/@oxygen/shared/dist/geo.d.ts +264 -0
  49. package/node_modules/@oxygen/shared/dist/geo.js +342 -0
  50. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
  51. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
  52. package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
  53. package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
  54. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.d.ts +19 -0
  55. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.js +25 -0
  56. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
  57. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +4 -0
  58. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
  59. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
  60. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
  61. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
  62. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +2 -0
  63. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +2 -0
  64. package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
  65. package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
  66. package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
  67. package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
  68. package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
  69. package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
  70. package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
  71. package/node_modules/@oxygen/shared/dist/version.d.ts +2 -1
  72. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  73. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  74. package/node_modules/@oxygen/shared/dist/version.js +4 -1
  75. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +46 -0
  76. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +51 -1
  77. package/node_modules/@oxygen/shared/package.json +10 -0
  78. package/package.json +1 -1
@@ -0,0 +1,342 @@
1
+ import { OxygenError } from "./cli-result.js";
2
+ import { resolveDeployEnv } from "./deploy-env.js";
3
+ export const GEO_ENGINES = [
4
+ "chatgpt",
5
+ "gemini",
6
+ "google-ai-mode",
7
+ "google-ai-overview",
8
+ "perplexity",
9
+ "copilot",
10
+ "claude",
11
+ ];
12
+ /** Binary exports carry this metadata to web, CLI and MCP, even for CSV/PDF. */
13
+ export function readGeoExportPagination(headers) {
14
+ const value = headers.get("x-oxygen-export-pagination");
15
+ if (!value || value.length > 2048)
16
+ return undefined;
17
+ try {
18
+ const data = JSON.parse(value);
19
+ if (!Number.isSafeInteger(data.returned) || data.returned < 0 ||
20
+ !Number.isSafeInteger(data.total) || data.total < data.returned ||
21
+ !(data.next_cursor === null || typeof data.next_cursor === "string" && /^[0-9a-f-]{36}$/i.test(data.next_cursor)) ||
22
+ data.summary_scope !== "all_filtered_answers")
23
+ return undefined;
24
+ return data;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ export const GEO_PILOT_CAP_MICROS = 10_000_000;
31
+ /** The initial release gate is independent of NODE_ENV and protects shadow data. */
32
+ export function isGeoEnabled(env = process.env) {
33
+ return resolveDeployEnv(env).tier === "dev";
34
+ }
35
+ export function assertGeoEnabled(env = process.env) {
36
+ if (!isGeoEnabled(env))
37
+ throw new OxygenError("geo_unavailable", "GEO / AEO is available to staff on dev while it is being tested.", { exitCode: 1 });
38
+ }
39
+ export function geoDomain(value) {
40
+ try {
41
+ const url = new URL(value.includes("://") ? value : `https://${value}`);
42
+ if (!["http:", "https:"].includes(url.protocol) ||
43
+ url.username ||
44
+ url.password)
45
+ return null;
46
+ return url.hostname
47
+ .toLowerCase()
48
+ .replace(/^www\./, "")
49
+ .replace(/\.$/, "");
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ function matchedTerm(text, term) {
56
+ const escaped = term.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57
+ if (!escaped)
58
+ return -1;
59
+ const match = new RegExp(`(^|[^\\p{L}\\p{N}])(${escaped})(?=$|[^\\p{L}\\p{N}])`, "iu").exec(text);
60
+ return match ? match.index + (match[1]?.length ?? 0) : -1;
61
+ }
62
+ export function analyzeGeoAnswer(text, brand, competitors, urls) {
63
+ const prose = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_link, label, url) => {
64
+ // A domain-only citation label is source attribution, not a recommendation.
65
+ return label.trim().includes(".") &&
66
+ geoDomain(label.trim()) === geoDomain(url)
67
+ ? ""
68
+ : label;
69
+ });
70
+ const subjects = [brand, ...competitors];
71
+ const mentions = subjects.map((subject) => {
72
+ const indices = [subject.name, ...subject.aliases, ...subject.domains]
73
+ .map((term) => matchedTerm(prose, term))
74
+ .filter((index) => index >= 0);
75
+ const first = indices.length ? Math.min(...indices) : -1;
76
+ let rank = null;
77
+ for (const line of prose.split("\n")) {
78
+ const list = /^\s*(\d{1,2})[.)]\s+(.+)$/.exec(line);
79
+ if (list &&
80
+ [subject.name, ...subject.aliases].some((term) => matchedTerm(list[2] ?? "", term) >= 0)) {
81
+ rank = Number(list[1]);
82
+ break;
83
+ }
84
+ }
85
+ return {
86
+ brand: subject.name,
87
+ present: first >= 0,
88
+ evidence: first >= 0 ? prose.slice(Math.max(0, first - 60), first + 180) : null,
89
+ recommendation_rank: rank,
90
+ };
91
+ });
92
+ const citations = [];
93
+ const seen = new Set();
94
+ for (const raw of urls) {
95
+ try {
96
+ const url = new URL(raw);
97
+ const domain = geoDomain(raw);
98
+ if (!domain)
99
+ continue;
100
+ url.hash = "";
101
+ for (const key of [...url.searchParams.keys()])
102
+ if (/^(utm_|fbclid|gclid)/i.test(key))
103
+ url.searchParams.delete(key);
104
+ const normalized = url.href;
105
+ if (seen.has(normalized))
106
+ continue;
107
+ seen.add(normalized);
108
+ const belongs = (subject) => subject.domains.some((d) => {
109
+ const owned = geoDomain(d);
110
+ return owned && (domain === owned || domain.endsWith(`.${owned}`));
111
+ });
112
+ const category = belongs(brand)
113
+ ? "owned"
114
+ : competitors.some(belongs)
115
+ ? "competitor"
116
+ : /(^|\.)(reddit\.com|linkedin\.com|youtube\.com|quora\.com)$/.test(domain)
117
+ ? "community"
118
+ : /(^|\.)(g2\.com|capterra\.com|trustpilot\.com)$/.test(domain)
119
+ ? "review"
120
+ : "other";
121
+ citations.push({
122
+ url: normalized,
123
+ domain,
124
+ position: citations.length + 1,
125
+ category,
126
+ });
127
+ }
128
+ catch {
129
+ /* An invalid provider URL is retained in raw evidence, not made clickable. */
130
+ }
131
+ }
132
+ return {
133
+ mentions,
134
+ citations,
135
+ measurement: structuredClone({ brand, competitors }),
136
+ };
137
+ }
138
+ /** Exact capture-time configuration, canonicalized for matched comparisons. */
139
+ export function geoMeasurementKey(answer) {
140
+ if (!answer.measurement)
141
+ return "unknown";
142
+ const subject = (value) => ({
143
+ name: value.name.trim().toLowerCase(),
144
+ aliases: [
145
+ ...new Set(value.aliases.map((a) => a.trim().toLowerCase())),
146
+ ].sort(),
147
+ domains: [
148
+ ...new Set(value.domains.map((d) => geoDomain(d) ?? d.toLowerCase())),
149
+ ].sort(),
150
+ });
151
+ return JSON.stringify({
152
+ brand: subject(answer.measurement.brand),
153
+ competitors: answer.measurement.competitors
154
+ .map(subject)
155
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
156
+ });
157
+ }
158
+ /** A scraper's question heading is not answer prose, including old stored captures. */
159
+ export function isGeoPromptEcho(text, prompt) {
160
+ const normalized = (value) => value.replace(/^[#>\s]+/gm, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
161
+ return Boolean(normalized(prompt)) && normalized(text) === normalized(prompt);
162
+ }
163
+ export function isGeoControlledAnswer(answer) {
164
+ return answer.collection === "consumer" && answer.outcome === "answer" &&
165
+ typeof answer.text === "string" && Boolean(answer.text.trim()) &&
166
+ !isGeoPromptEcho(answer.text, answer.prompt ?? "");
167
+ }
168
+ export function summarizeGeoAnswers(answers, brand, failedSamples = 0) {
169
+ const samples = answers.filter(isGeoControlledAnswer);
170
+ const measurementContexts = new Set(samples.map(geoMeasurementKey)).size;
171
+ const comparable = measurementContexts <= 1;
172
+ const brands = new Map();
173
+ for (const sample of samples) {
174
+ const presentBrands = new Set(sample.mentions
175
+ .filter((m) => m.present)
176
+ .map((m) => m.brand === sample.measurement?.brand.name ? brand : m.brand));
177
+ for (const name of presentBrands)
178
+ brands.set(name, (brands.get(name) ?? 0) + 1);
179
+ }
180
+ const totalMentions = [...brands.values()].reduce((a, b) => a + b, 0);
181
+ const present = brands.get(brand) ?? 0;
182
+ const cited = samples.filter((a) => a.citations.some((c) => c.category === "owned")).length;
183
+ const sources = new Map();
184
+ for (const answer of samples)
185
+ for (const citation of answer.citations) {
186
+ const source = sources.get(citation.domain) ?? {
187
+ domain: citation.domain,
188
+ count: 0,
189
+ category: citation.category,
190
+ urls: [],
191
+ };
192
+ source.count++;
193
+ if (!source.urls.includes(citation.url))
194
+ source.urls.push(citation.url);
195
+ sources.set(citation.domain, source);
196
+ }
197
+ return {
198
+ successful_samples: samples.length,
199
+ measurement_contexts: measurementContexts,
200
+ failed_samples: failedSamples,
201
+ invalid_answer_samples: answers.filter((a) => a.collection === "consumer" && a.outcome === "answer" && !isGeoControlledAnswer(a)).length,
202
+ imported_samples: answers.filter((a) => a.collection === "import").length,
203
+ no_overview_samples: answers.filter((a) => a.outcome === "no_overview")
204
+ .length,
205
+ mentioned_samples: present,
206
+ visibility: comparable && samples.length ? present / samples.length : null,
207
+ cited_samples: cited,
208
+ citation_coverage: comparable && samples.length ? cited / samples.length : null,
209
+ share_of_voice: comparable && totalMentions ? present / totalMentions : null,
210
+ competitors: [...brands].map(([name, count]) => ({
211
+ name,
212
+ mentioned_samples: count,
213
+ share_of_voice: comparable && totalMentions ? count / totalMentions : null,
214
+ })),
215
+ sources: [...sources.values()].sort((a, b) => b.count - a.count),
216
+ };
217
+ }
218
+ /** No fetching or execution: customer wording and citations are evidence only. */
219
+ export function parseGeoTranscript(input, redactions = []) {
220
+ if (new TextEncoder().encode(input).byteLength > 1_000_000)
221
+ throw new OxygenError("geo_import_too_large", "Import one conversation of at most 1 MB.", { exitCode: 2 });
222
+ const redact = (value) => redactions.reduce((text, term) => term.trim() ? text.split(term).join("[redacted]") : text, value);
223
+ const messages = [];
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(input);
227
+ }
228
+ catch {
229
+ parsed = null;
230
+ }
231
+ if (Array.isArray(parsed) && parsed.length > 1)
232
+ throw new OxygenError("geo_conversation_required", "Import one selected conversation instead of a full account export.", { exitCode: 2 });
233
+ const root = Array.isArray(parsed) ? parsed[0] : parsed;
234
+ if (root && typeof root === "object") {
235
+ const data = root;
236
+ if (data.mapping && typeof data.mapping === "object") {
237
+ const mapping = data.mapping;
238
+ const branch = [];
239
+ let node = typeof data.current_node === "string" ? data.current_node : null;
240
+ if (!node || !Object.hasOwn(mapping, node))
241
+ throw new OxygenError("geo_conversation_branch_required", "Import a conversation export with a valid selected branch.", { exitCode: 2 });
242
+ const visited = new Set();
243
+ while (node) {
244
+ if (!Object.hasOwn(mapping, node) || visited.has(node) || !mapping[node] || typeof mapping[node] !== "object")
245
+ throw new OxygenError("geo_conversation_branch_invalid", "The selected conversation branch is incomplete or invalid.", { exitCode: 2 });
246
+ visited.add(node);
247
+ const entry = mapping[node];
248
+ branch.unshift(entry);
249
+ node = entry.parent ?? null;
250
+ }
251
+ for (const entry of branch) {
252
+ const role = entry.message?.author?.role;
253
+ if (role === "user" || role === "assistant")
254
+ messages.push({
255
+ role,
256
+ text: (entry.message?.content?.parts ?? [])
257
+ .filter((x) => typeof x === "string")
258
+ .join("\n"),
259
+ });
260
+ }
261
+ if (!messages.length)
262
+ throw new OxygenError("geo_conversation_branch_invalid", "The selected conversation branch contains no readable messages.", { exitCode: 2 });
263
+ }
264
+ else {
265
+ const rows = data.chat_messages ?? data.messages;
266
+ if (Array.isArray(rows))
267
+ for (const row of rows) {
268
+ if (!row || typeof row !== "object")
269
+ continue;
270
+ const role = row.sender === "human" || row.role === "user"
271
+ ? "user"
272
+ : row.sender === "assistant" || row.role === "assistant"
273
+ ? "assistant"
274
+ : null;
275
+ if (role)
276
+ messages.push({
277
+ role,
278
+ text: typeof row.text === "string"
279
+ ? row.text
280
+ : typeof row.content === "string"
281
+ ? row.content
282
+ : Array.isArray(row.content)
283
+ ? row.content
284
+ .map((c) => c.text ?? "")
285
+ .join("\n")
286
+ : "",
287
+ });
288
+ }
289
+ }
290
+ }
291
+ if (!messages.length)
292
+ messages.push({ role: "user", text: input });
293
+ for (const message of messages)
294
+ message.text = redact(message.text);
295
+ // Retain only the selected conversation branch and reviewed text. Hidden JSON
296
+ // metadata, sibling chats, attachments and discarded branches are never saved.
297
+ return {
298
+ messages,
299
+ prompts: messages
300
+ .filter((m) => m.role === "user")
301
+ .map((m) => m.text)
302
+ .filter(Boolean),
303
+ sources: [
304
+ ...new Set(messages.flatMap((m) => m.text.match(/https?:\/\/[^\s<>"\])]+/g) ?? [])),
305
+ ],
306
+ redacted_text: messages.map((m) => `${m.role}: ${m.text}`).join("\n\n"),
307
+ };
308
+ }
309
+ /** Compare only common collection cohorts; imports and missing AIOs are excluded. */
310
+ export function compareGeoExperiment(before, after, brand) {
311
+ const eligible = (answers) => answers.filter((a) => isGeoControlledAnswer(a) &&
312
+ a.measurement !== undefined);
313
+ const key = (a) => JSON.stringify([
314
+ a.prompt_id,
315
+ a.prompt_revision ?? null,
316
+ a.engine,
317
+ a.provider,
318
+ a.market?.country,
319
+ a.market?.language,
320
+ geoMeasurementKey(a),
321
+ ]);
322
+ const baseline = eligible(before);
323
+ const followup = eligible(after);
324
+ const baselineKeys = new Set(baseline.map(key));
325
+ const followupKeys = new Set(followup.map(key));
326
+ const common = [...baselineKeys].filter((k) => followupKeys.has(k));
327
+ const matchedBefore = baseline.filter((a) => common.includes(key(a)));
328
+ const matchedAfter = followup.filter((a) => common.includes(key(a)));
329
+ return {
330
+ before: summarizeGeoAnswers(matchedBefore, brand),
331
+ after: summarizeGeoAnswers(matchedAfter, brand),
332
+ comparable_cohorts: common.length,
333
+ excluded_before: before.length - matchedBefore.length,
334
+ excluded_after: after.length - matchedAfter.length,
335
+ cohorts: common.map((k) => ({
336
+ cohort: JSON.parse(k),
337
+ before: summarizeGeoAnswers(baseline.filter((a) => key(a) === k), brand),
338
+ after: summarizeGeoAnswers(followup.filter((a) => key(a) === k), brand),
339
+ })),
340
+ interpretation: "Observed association within matching prompt revision, engine, provider, locale and capture-time brand definitions. Unknown definitions are excluded. Compare individual cohorts; changing their sample mix can change aggregate rates. This does not establish causation.",
341
+ };
342
+ }
@@ -20,6 +20,15 @@
20
20
  */
21
21
  /** Canonical LinkedIn country name for each ISO 3166-1 alpha-2 code, A→Z by name. */
22
22
  export declare const LINKEDIN_COUNTRIES: ReadonlyArray<readonly [code: string, name: string]>;
23
+ /**
24
+ * Spellings a provider may return that are not the canonical name. Lowercased
25
+ * keys; the resolver lowercases its input before the lookup. Kept deliberately
26
+ * short — it covers the CLDR spellings this table overrides (so a payload
27
+ * carrying the un-overridden name still resolves), the common colloquial forms,
28
+ * and the alpha-3 codes, because Up2Data's `hq.country` is not contractually
29
+ * alpha-2 and has been observed carrying a display name.
30
+ */
31
+ export declare const COUNTRY_ALIASES: Readonly<Record<string, string>>;
23
32
  /**
24
33
  * Resolve a provider value — an alpha-2 code, a canonical name, or a known
25
34
  * alias — to its canonical LinkedIn country name.
@@ -279,7 +279,7 @@ export const LINKEDIN_COUNTRIES = [
279
279
  * and the alpha-3 codes, because Up2Data's `hq.country` is not contractually
280
280
  * alpha-2 and has been observed carrying a display name.
281
281
  */
282
- const COUNTRY_ALIASES = {
282
+ export const COUNTRY_ALIASES = {
283
283
  // CLDR spellings this table overrides.
284
284
  "hong kong sar china": "HK",
285
285
  "macao sar china": "MO",
@@ -0,0 +1,15 @@
1
+ /** Public vocabulary for OXYGEN-managed email services. Backend ids stay in storage. */
2
+ export type ManagedEmailProduct = "warmup" | "placement";
3
+ export declare const OXYGEN_WARMUP_PROVIDER = "oxygen_warmup";
4
+ export declare const OXYGEN_PLACEMENT_PROVIDER = "oxygen_placement";
5
+ export declare function managedEmailProviderInput(value: unknown, product: ManagedEmailProduct): unknown;
6
+ export declare function isManagedWarmupProvider(value: unknown): boolean;
7
+ export declare function isManagedPlacementProvider(value: unknown): boolean;
8
+ export declare function managedEmailMessage(value: string, product?: ManagedEmailProduct): string;
9
+ /**
10
+ * Project only service identifiers and copy. Preserve URLs, ids, hashes, dates,
11
+ * customer-authored subject/body/configuration, and provider evidence verbatim.
12
+ * The caller must choose a managed email surface; do not apply to generic BYOK tools.
13
+ */
14
+ export declare function publicManagedEmail<T>(value: T, product?: ManagedEmailProduct): T;
15
+ export declare function publicManagedEmailCommand<T>(command: string, value: T): T;
@@ -0,0 +1,81 @@
1
+ export const OXYGEN_WARMUP_PROVIDER = "oxygen_warmup";
2
+ export const OXYGEN_PLACEMENT_PROVIDER = "oxygen_placement";
3
+ export function managedEmailProviderInput(value, product) {
4
+ const publicId = product === "warmup" ? OXYGEN_WARMUP_PROVIDER : OXYGEN_PLACEMENT_PROVIDER;
5
+ return typeof value === "string" && value.trim().toLowerCase() === publicId ? "sendkit" : value;
6
+ }
7
+ export function isManagedWarmupProvider(value) {
8
+ return value === "sendkit" || value === OXYGEN_WARMUP_PROVIDER;
9
+ }
10
+ export function isManagedPlacementProvider(value) {
11
+ return value === "sendkit" || value === OXYGEN_PLACEMENT_PROVIDER;
12
+ }
13
+ // These are service-owned fields, never arbitrary customer content or opaque receipts.
14
+ const IDENTIFIER_FIELDS = new Set([
15
+ "provider", "active_provider", "warmup_provider", "warmupProvider", "warmupRail",
16
+ "action", "code", "failure_code", "error_code", "credential_source", "credentialSource",
17
+ "method", "handoff", "reason_code", "kind", "type", "source",
18
+ ]);
19
+ const COPY_FIELDS = new Set([
20
+ "message", "note", "reason", "description", "hint", "transport_hint", "next_step",
21
+ "next_action", "nextAction", "label", "title", "instruction", "instructions", "warnings",
22
+ "recommendation", "recommendations", "limitations", "next_external_write", "price_note",
23
+ "error", "errors", "summary", "blocker", "blockers", "detail", "details_text", "provider_label",
24
+ ]);
25
+ export function managedEmailMessage(value, product = "warmup") {
26
+ const label = product === "warmup" ? "OXYGEN Warm-up" : "OXYGEN placement evaluation";
27
+ if (product === "placement") {
28
+ value = value.replace(/(?:SendKit|OXYGEN placement evaluation) does not document whether inbox, spam, and missing summary values are counts or percentages; Oxygen preserves the values without conversion\./gi, "Summary units are unverified (counts or percentages); values are shown as recorded, without conversion.");
29
+ }
30
+ return value.replace(/SendKit(?:'s)?\s+warm-?up/gi, label).replace(/SendKit/gi, label);
31
+ }
32
+ /**
33
+ * Project only service identifiers and copy. Preserve URLs, ids, hashes, dates,
34
+ * customer-authored subject/body/configuration, and provider evidence verbatim.
35
+ * The caller must choose a managed email surface; do not apply to generic BYOK tools.
36
+ */
37
+ export function publicManagedEmail(value, product = "warmup") {
38
+ const opaqueFields = new Set(["plan_hash", "id", "subject", "body", "body_text", "body_html", "config", "warmupConfig", "rampConfig", "email_address", "emailAddress", "tags", "displayName", "signatureHtml"]);
39
+ function project(entry, field = "", context = product) {
40
+ const publicId = context === "warmup" ? OXYGEN_WARMUP_PROVIDER : OXYGEN_PLACEMENT_PROVIDER;
41
+ if (opaqueFields.has(field))
42
+ return entry;
43
+ if (typeof entry === "string") {
44
+ if (field === "source_id" || field === "sourceId") {
45
+ return entry.replace(/^warmup:sendkit:/, "warmup:oxygen_warmup:");
46
+ }
47
+ if (IDENTIFIER_FIELDS.has(field)) {
48
+ if (entry.toLowerCase() === "sendkit")
49
+ return publicId;
50
+ return entry.replace(/sendkit_/gi, `${publicId}_`).replace(/_sendkit/gi, `_${publicId}`);
51
+ }
52
+ return COPY_FIELDS.has(field) ? managedEmailMessage(entry, context) : entry;
53
+ }
54
+ if (Array.isArray(entry))
55
+ return entry.map((item) => project(item, field, context));
56
+ if (!entry || typeof entry !== "object" || entry instanceof Date)
57
+ return entry;
58
+ const record = entry;
59
+ // Existing customer-owned bindings stay private; never label their usage managed warmup.
60
+ const groupKey = record.key && typeof record.key === "object" && !Array.isArray(record.key)
61
+ ? record.key : null;
62
+ const privateUsage = (record.provider === "sendkit" || groupKey?.provider === "sendkit") &&
63
+ (record.type === "byok_usage" || (typeof record.byok_usage_events === "number" && record.byok_usage_events > 0));
64
+ return Object.fromEntries(Object.entries(record).map(([key, item]) => [
65
+ { sendkit_google: "oxygen_warmup_google", sendkit_microsoft: "oxygen_warmup_microsoft", sendkit_binding: "oxygen_placement_binding", sendkitBinding: "oxygen_placement_binding" }[key] ?? key,
66
+ privateUsage && key === "provider" ? "private_integration"
67
+ : privateUsage && key === "key" ? project({ ...groupKey, provider: "private_integration" }, key, context)
68
+ : privateUsage && key === "tool_id" ? null
69
+ : privateUsage && key === "description" ? "Existing private integration usage."
70
+ : project(item, key, /^warmup/i.test(key) ? "warmup" : /^placement/i.test(key) ? "placement" : context),
71
+ ]));
72
+ }
73
+ return project(value);
74
+ }
75
+ export function publicManagedEmailCommand(command, value) {
76
+ if (/^(mailboxes|managed-inboxes|billing)(?: |$)/.test(command))
77
+ return publicManagedEmail(value);
78
+ if (/^deliverability(?: |$)/.test(command))
79
+ return publicManagedEmail(value, "placement");
80
+ return value;
81
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The table the onboarding consultant saves its ranked first leads into (founder, current
3
+ * chat, 2026-10-09), so a first session leaves a durable record the user can open, enrich
4
+ * and enroll instead of a chat transcript. The onboarding read (web) and the ChatGPT
5
+ * connector's onboarding hint (MCP) both build it from here; the served onboarding skill
6
+ * shows the same shape, and `apps/web/src/lib/agent-skills.test.ts` holds it to this one.
7
+ *
8
+ * The upsert refuses a row without its key, so every row carries `lead_key`: the LinkedIn
9
+ * profile URL, or the email when there is no profile. The key also keeps a re-run from
10
+ * adding anyone twice. Creating the table and writing its rows cost 0 credits.
11
+ */
12
+ export declare const FIRST_LEADS_TABLE = "First leads";
13
+ export declare const FIRST_LEADS_UPSERT_KEY = "lead_key";
14
+ export type FirstLeadsColumn = {
15
+ key: string;
16
+ label: string;
17
+ dataType: "text";
18
+ };
19
+ export declare const FIRST_LEADS_COLUMNS: readonly FirstLeadsColumn[];
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The table the onboarding consultant saves its ranked first leads into (founder, current
3
+ * chat, 2026-10-09), so a first session leaves a durable record the user can open, enrich
4
+ * and enroll instead of a chat transcript. The onboarding read (web) and the ChatGPT
5
+ * connector's onboarding hint (MCP) both build it from here; the served onboarding skill
6
+ * shows the same shape, and `apps/web/src/lib/agent-skills.test.ts` holds it to this one.
7
+ *
8
+ * The upsert refuses a row without its key, so every row carries `lead_key`: the LinkedIn
9
+ * profile URL, or the email when there is no profile. The key also keeps a re-run from
10
+ * adding anyone twice. Creating the table and writing its rows cost 0 credits.
11
+ */
12
+ export const FIRST_LEADS_TABLE = "First leads";
13
+ export const FIRST_LEADS_UPSERT_KEY = "lead_key";
14
+ export const FIRST_LEADS_COLUMNS = [
15
+ ["name", "Name"],
16
+ ["title", "Title"],
17
+ ["company", "Company"],
18
+ ["linkedin_url", "LinkedIn URL"],
19
+ ["email", "Email"],
20
+ [FIRST_LEADS_UPSERT_KEY, "Lead key"],
21
+ ["why_warm", "Why warm"],
22
+ ["icp_fit", "ICP fit"],
23
+ ["next_step", "Next step"],
24
+ ["draft_message", "Draft message"],
25
+ ].map(([key, label]) => ({ key, label, dataType: "text" }));
@@ -60,7 +60,7 @@ export type OtlpLogSinkOptions = {
60
60
  * `worker.log_shipper.stats` is the Axiom shipper's once-a-minute loss accounting,
61
61
  * which by design never reaches Axiom (OXP-1.6.126).
62
62
  */
63
- export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume", "worker.log_shipper.stats"];
63
+ export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["diagnostics.capture_stored", "diagnostics.invocation_completed", "diagnostics.canary_completed", "diagnostics.digest_completed", "tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume", "worker.log_shipper.stats"];
64
64
  export type OtlpLogSinkHandle = {
65
65
  sink: LogSink;
66
66
  flush: () => Promise<void>;
@@ -12,6 +12,10 @@ import { telemetryEndpointForLog } from "./telemetry-export-observer.js";
12
12
  * which by design never reaches Axiom (OXP-1.6.126).
13
13
  */
14
14
  export const OTLP_MONITOR_INFO_EVENTS = [
15
+ "diagnostics.capture_stored",
16
+ "diagnostics.invocation_completed",
17
+ "diagnostics.canary_completed",
18
+ "diagnostics.digest_completed",
15
19
  "tenant.migrations.cron.done",
16
20
  "outbound_send.sent",
17
21
  "worker_workflow_cron_overlap_skipped",
@@ -137,11 +137,7 @@ export declare const RESEARCH_DEPTH_MODEL_CREDITS: {
137
137
  readonly standard: 4.3;
138
138
  readonly deep: 7.6;
139
139
  };
140
- /** The tier whose model each loop depth runs on, whatever tier the column stores. */
141
- export declare const RESEARCH_DEPTH_MODEL_TIER: {
142
- readonly standard: "low";
143
- readonly deep: "medium";
144
- };
140
+ /** The tier whose model each loop depth runs on lives in ./research-depth.ts: a routing fact, not a price. */
145
141
  /**
146
142
  * The tool half of a standard/deep row. `typical` is the ratified planning shape —
147
143
  * standard: 2 serper searches + 2 native page reads; deep: 3 serper searches + 4
@@ -177,18 +173,18 @@ export declare const RESEARCH_DEPTH_TYPICAL_ROW_CREDITS: {
177
173
  */
178
174
  export { AUTOMATION_STEP_CREDITS_REPRICING } from "./repricing.js";
179
175
  /**
180
- * The flat mailbox line of a managed Google inbox after the 2026-09 repricing
181
- * (400, decision 3.1, slice S32). Its 100-credit mailbox connection, warm-up
182
- * included, bills beside it: 500 all-in. The all-in price waits for the S07
183
- * switch, so a surface that quotes it uses `managedInboxAllInCredits()` from
184
- * `@oxygen/shared/repricing`, resolved on the server. Kept as an object
185
- * (min/typical/max all equal) so callers that destructure any field need no
186
- * shape change.
176
+ * The flat mailbox line of a managed Google or Microsoft 365 inbox: 300 (founder,
177
+ * 2026-10-07; repricing decision 3.1 had set 400 for Google alone). Its 100-credit
178
+ * mailbox connection, warm-up included, bills beside it: 400 all-in, which
179
+ * `managedInboxAllInCredits()` from `@oxygen/shared/repricing` quotes. The price
180
+ * charged is the pricing book's row; this is its committed default. Kept as an
181
+ * object (min/typical/max all equal) so callers that destructure any field need
182
+ * no shape change.
187
183
  */
188
184
  export declare const MANAGED_INBOX_CREDITS_PER_MONTH: {
189
- readonly min: 400;
190
- readonly typical: 400;
191
- readonly max: 400;
185
+ readonly min: 300;
186
+ readonly typical: 300;
187
+ readonly max: 300;
192
188
  };
193
189
  /**
194
190
  * VOICE (the call channel). One price per connected minute, 5 credits ($0.05),
@@ -221,17 +217,17 @@ export declare const SENDING_DOMAIN_CREDITS_PER_YEAR_TYPICAL = 1250;
221
217
  * THE ALLOWANCE IS LOAD-BEARING, not a perk. Placement COGS is per TEST
222
218
  * ($0.1327 on EmailGuard's Agency plan), not per inbox, so an unbounded seat
223
219
  * inverts its own margin: at $1.00/inbox-month the EIGHTH test in a cycle was
224
- * already sold at a loss. Two included tests cost $0.265 against $1.35 of
225
- * revenue at the 2026-09 price, and everything past them bills
220
+ * already sold at a loss. Two included tests cost $0.265 against $1.00 of
221
+ * revenue at today's price, and everything past them bills
226
222
  * PLACEMENT_TEST_OVERAGE_CREDITS. Orgs on their own EmailGuard key (BYOK) pay 0.
227
223
  *
228
- * These two prices are the RATIFIED list prices from the 2026-09 repricing
229
- * (135 and 66, slice S30). The rises wait for the S07 effective-date switch, so
230
- * a surface that quotes or bills uses `deliverabilitySeatCredits()` and
231
- * `placementTestCredits()` from `@oxygen/shared/repricing` (100 and 16.6 until
232
- * the switch's instant), never these constants.
224
+ * Monitoring is 100 credits per inbox-month (founder, 2026-10-07; the 2026-09
225
+ * repricing had set 135). The extra test is the RATIFIED 66 (slice S30), whose rise
226
+ * waits for the S07 effective-date switch, so a surface that quotes or bills uses
227
+ * `deliverabilitySeatCredits()` and `placementTestCredits()` from
228
+ * `@oxygen/shared/repricing` (16.6 for the test until the switch's instant).
233
229
  */
234
- export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 135;
230
+ export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 100;
235
231
  export declare const DELIVERABILITY_INCLUDED_TESTS_PER_INBOX = 2;
236
232
  export declare const PLACEMENT_TEST_OVERAGE_CREDITS = 66;
237
233
  /**
@@ -262,7 +258,7 @@ export declare const LINKEDIN_TYPICAL_ACTIONS_CREDITS_PER_MONTH = 0;
262
258
  * Seeded at the 2026-09 repricing's price (3,000, decision 2.3); resolve the
263
259
  * price in force with repricedChargeCredits.
264
260
  */
265
- export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 3000;
261
+ export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 3400;
266
262
  /**
267
263
  * X accounts: monthly credit reservation per connected account (2026-09
268
264
  * repricing, decision 2.2). Like the LinkedIn and WhatsApp constants above this