@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.
- package/README.md +1 -1
- package/dist/command-manifest.d.ts +3 -1
- package/dist/command-manifest.js +11 -0
- package/dist/customer-operation-commands.js +7 -3
- package/dist/diagnostic-upload-main.d.ts +1 -0
- package/dist/diagnostic-upload-main.js +2 -0
- package/dist/diagnostic-uploader.d.ts +3 -0
- package/dist/diagnostic-uploader.js +68 -0
- package/dist/diagnostics.d.ts +22 -0
- package/dist/diagnostics.js +219 -0
- package/dist/http-client.d.ts +7 -0
- package/dist/http-client.js +12 -2
- package/dist/index.js +72 -34
- package/dist/transcript.js +1 -1
- package/node_modules/@oxygen/formula/dist/expression.js +16 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +15 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +28 -4
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/geo.js +160 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +10 -4
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
- package/node_modules/@oxygen/shared/dist/diagnostic-context.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-context.js +17 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +12 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +60 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-receipt.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-receipt.js +22 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +39 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +233 -0
- package/node_modules/@oxygen/shared/dist/diagnostics.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/diagnostics.js +253 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
- package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
- package/node_modules/@oxygen/shared/dist/geo.d.ts +264 -0
- package/node_modules/@oxygen/shared/dist/geo.js +342 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
- package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
- package/node_modules/@oxygen/shared/dist/onboarding-first-leads.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/onboarding-first-leads.js +25 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +4 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +2 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
- package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
- package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +4 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +46 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +51 -1
- package/node_modules/@oxygen/shared/package.json +10 -0
- 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
|
|
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
|
|
181
|
-
*
|
|
182
|
-
* included, bills beside it:
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* (min/typical/max all equal) so callers that destructure any field need
|
|
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:
|
|
190
|
-
readonly typical:
|
|
191
|
-
readonly max:
|
|
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.
|
|
225
|
-
* revenue at
|
|
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
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* a surface that quotes or bills uses
|
|
231
|
-
* `
|
|
232
|
-
* the switch's instant)
|
|
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 =
|
|
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 =
|
|
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
|