@oxygen-agent/cli 1.1012.2 → 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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/customer-operation-commands.js +6 -2
  3. package/dist/diagnostic-upload-main.d.ts +1 -0
  4. package/dist/diagnostic-upload-main.js +2 -0
  5. package/dist/diagnostic-uploader.d.ts +3 -0
  6. package/dist/diagnostic-uploader.js +68 -0
  7. package/dist/diagnostics.d.ts +22 -0
  8. package/dist/diagnostics.js +219 -0
  9. package/dist/http-client.d.ts +7 -0
  10. package/dist/http-client.js +12 -2
  11. package/dist/index.js +15 -3
  12. package/node_modules/@oxygen/shared/dist/capability-discovery.js +4 -4
  13. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -1
  14. package/node_modules/@oxygen/shared/dist/contracts/geo.js +30 -8
  15. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +1 -0
  16. package/node_modules/@oxygen/shared/dist/diagnostic-context.d.ts +3 -0
  17. package/node_modules/@oxygen/shared/dist/diagnostic-context.js +17 -0
  18. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +4 -0
  19. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +12 -0
  20. package/node_modules/@oxygen/shared/dist/diagnostic-lock.d.ts +2 -0
  21. package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +60 -0
  22. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.d.ts +6 -0
  23. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.js +22 -0
  24. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.d.ts +2 -0
  25. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +39 -0
  26. package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +31 -0
  27. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +233 -0
  28. package/node_modules/@oxygen/shared/dist/diagnostics.d.ts +49 -0
  29. package/node_modules/@oxygen/shared/dist/diagnostics.js +253 -0
  30. package/node_modules/@oxygen/shared/dist/geo.d.ts +17 -0
  31. package/node_modules/@oxygen/shared/dist/geo.js +31 -3
  32. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.d.ts +19 -0
  33. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.js +25 -0
  34. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
  35. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +4 -0
  36. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +2 -0
  37. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +2 -0
  38. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -0
  39. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/version.js +3 -0
  42. package/package.json +1 -1
@@ -0,0 +1,253 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { credentialBearingOperation } from "./diagnostic-credentials.js";
3
+ import { redactSecretsInString } from "./redaction.js";
4
+ /** Internal diagnostic evidence, distinct from customer run state and usage counts. */
5
+ export const DIAGNOSTIC_VERSION = 1;
6
+ export const DIAGNOSTIC_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
7
+ export const DIAGNOSTIC_INVOCATION_HEADER = "X-Oxygen-Invocation-Id";
8
+ export const DIAGNOSTIC_UPLOAD_CHUNK_BYTES = 1024 * 1024;
9
+ export const DIAGNOSTIC_MAX_BYTES = 512 * 1024 * 1024;
10
+ // Total spool capacity is separate from the bounded JSON materialization of one
11
+ // record. Larger contents explicitly become partial evidence, never a full claim.
12
+ export const DIAGNOSTIC_RECORD_MAX_BYTES = 16 * 1024 * 1024;
13
+ export const DIAGNOSTIC_UPLOAD_MAX_CHUNKS = 17; // Includes gzip/envelope overhead.
14
+ export function diagnosticId(value) {
15
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value.toLowerCase() : null;
16
+ }
17
+ export function diagnosticOutcome(ok, status, code) {
18
+ if (ok)
19
+ return "ok";
20
+ if (code && /^(network_|.*timeout$)/.test(code))
21
+ return "network_error";
22
+ if (code && /^(cancelled|aborted|interrupted)$/.test(code))
23
+ return "cancelled";
24
+ if (status === 401 || status === 403 || status === 402 || status === 409 || status === 429
25
+ || (code && /approval|budget|credit|quota|rate_limit|permission|forbidden|not_logged_in|unauthorized|update_required|read_only/.test(code)))
26
+ return "refused";
27
+ if ((status && status >= 400 && status < 500) || (code && /invalid|missing|unknown_(command|option|tool)|not_found|commander\./.test(code)))
28
+ return "invalid_input";
29
+ return "platform_error";
30
+ }
31
+ export function createDiagnosticRecord(input) {
32
+ const id = diagnosticId(input.id) ?? randomUUID();
33
+ return {
34
+ version: DIAGNOSTIC_VERSION, id, invocation_id: diagnosticId(input.invocation_id) ?? id,
35
+ parent_id: input.parent_id ?? null, layer: input.layer ?? "invocation", surface: input.surface,
36
+ operation: input.operation, started_at: input.started_at ?? new Date().toISOString(),
37
+ ended_at: input.ended_at ?? null, outcome: input.outcome ?? "pending", error_code: input.error_code ?? null,
38
+ status: input.status ?? null, organization_id: input.organization_id ?? null, user_id: input.user_id ?? null,
39
+ identity_verified: input.identity_verified ?? false, client_version: input.client_version ?? null,
40
+ trace_id: input.trace_id ?? null, traceparent: input.traceparent ?? null,
41
+ capture_state: input.capture_state ?? "complete", input: input.input ?? null, output: input.output ?? null,
42
+ metadata: input.metadata ?? {},
43
+ };
44
+ }
45
+ const SECRET_FIELD = /(?:^|_)(?:authorization|cookie|cookies|password|passwd|passphrase|secret|api_key|access_key|private_key|token|otp|security_code|connection_string|ciphertext|dsn|connection_uri|database_url|x_key|signature)(?:$|_)/i;
46
+ function credentialField(key) {
47
+ return SECRET_FIELD.test(key.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[- .]/g, "_"));
48
+ }
49
+ function binaryField(key) {
50
+ return /(?:^|_)(?:base64|b64)(?:$|_)/i.test(key.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[- .]/g, "_"));
51
+ }
52
+ function binaryMetadata(value) {
53
+ return { withheld: "binary", bytes: Buffer.byteLength(value, "base64") };
54
+ }
55
+ export function sanitizeDiagnosticText(value, knownSecrets = []) {
56
+ // CLI stdout and MCP text often contain serialized JSON rather than objects.
57
+ // Filter its credential fields without changing the surrounding business text.
58
+ for (const secret of knownSecrets)
59
+ if (secret)
60
+ value = value.split(secret).join("[credential excluded]");
61
+ // Text blocks may contain serialized tool results, including binary blocks.
62
+ try {
63
+ const parsed = JSON.parse(value);
64
+ if (parsed && typeof parsed === "object") {
65
+ const sanitized = sanitizeDiagnosticContent(parsed, knownSecrets);
66
+ if (JSON.stringify(parsed) !== JSON.stringify(sanitized))
67
+ value = JSON.stringify(sanitized);
68
+ }
69
+ }
70
+ catch { /* preserve ordinary business text */ }
71
+ value = value.replace(/data:[\w/+.-]+(?:;[\w=.-]+)*;base64,[A-Za-z0-9+/=]+/g, match => JSON.stringify(binaryMetadata(match.slice(match.indexOf(",") + 1))));
72
+ const redacted = redactSecretsInString(value)
73
+ .replace(/([?&#][\w.-]*(?:api[_-]?key|token|secret|password|signature|credential|code|auth)[\w.-]*=)[^&#\s"'<>]+/gi, "$1[credential excluded]")
74
+ .replace(/\b(?:oxy_(?:live|sess|user)_|gh[pousr]_|xox[baprs]-)[A-Za-z0-9_.-]+/g, "[credential excluded]");
75
+ return redacted.replace(/("(?:[^"\\]|\\.)*"\s*:\s*)("(?:[^"\\]|\\.)*")/g, (match, prefix) => {
76
+ try {
77
+ return credentialField(JSON.parse(prefix.slice(0, prefix.lastIndexOf(":"))).trim()) ? `${prefix}"[credential excluded]"` : match;
78
+ }
79
+ catch {
80
+ return match;
81
+ }
82
+ });
83
+ }
84
+ /** No business-content, array, string, field-count or depth truncation. */
85
+ export function sanitizeDiagnosticContent(value, knownSecrets = []) {
86
+ const ancestors = new Set();
87
+ const walk = (item) => {
88
+ if (typeof item === "string")
89
+ return sanitizeDiagnosticText(item, knownSecrets);
90
+ if (item === null || typeof item === "number" || typeof item === "boolean")
91
+ return item;
92
+ if (item === undefined)
93
+ return null;
94
+ if (typeof item !== "object")
95
+ throw new Error("Non-JSON diagnostic value");
96
+ if (ancestors.has(item))
97
+ throw new Error("Cyclic diagnostic value");
98
+ ancestors.add(item);
99
+ try {
100
+ if (item instanceof Date)
101
+ return item.toISOString();
102
+ if (item instanceof Error)
103
+ return walk({ name: item.name, message: item.message, stack: item.stack });
104
+ if (ArrayBuffer.isView(item) || item instanceof ArrayBuffer)
105
+ return { withheld: "binary", bytes: item.byteLength };
106
+ if (Array.isArray(item))
107
+ return item.map(walk);
108
+ if (("type" in item && (item.type === "image" || item.type === "audio")) && "data" in item && typeof item.data === "string") {
109
+ return { type: item.type, withheld: "binary", bytes: Buffer.byteLength(item.data, "base64"), ...("mimeType" in item ? { mimeType: item.mimeType } : {}) };
110
+ }
111
+ const result = Object.create(null);
112
+ for (const [key, entry] of Object.entries(item)) {
113
+ const encoded = typeof entry === "string" && (binaryField(key)
114
+ || ((key === "data" || key === "content") && "encoding" in item && item.encoding === "base64")
115
+ || (key === "content" && "filename" in item && "content_type" in item)
116
+ || (key === "blob" && "uri" in item && typeof item.uri === "string"));
117
+ result[key] = credentialField(key) ? "[credential excluded]" : encoded ? binaryMetadata(entry) : walk(entry);
118
+ }
119
+ return result;
120
+ }
121
+ finally {
122
+ ancestors.delete(item);
123
+ }
124
+ };
125
+ return walk(value);
126
+ }
127
+ export function sanitizeDiagnosticArgv(argv) {
128
+ let credentialValue = false;
129
+ let binaryValue = false;
130
+ return argv.map((argument) => {
131
+ if (credentialValue) {
132
+ credentialValue = false;
133
+ return "[credential excluded]";
134
+ }
135
+ if (binaryValue) {
136
+ binaryValue = false;
137
+ return JSON.stringify(binaryMetadata(argument));
138
+ }
139
+ if (argument.startsWith("--")) {
140
+ const [flag] = argument.slice(2).split("=");
141
+ if (credentialField(flag)) {
142
+ if (argument.includes("="))
143
+ return argument.slice(0, argument.indexOf("=") + 1) + "[credential excluded]";
144
+ credentialValue = true;
145
+ }
146
+ else if (binaryField(flag)) {
147
+ if (argument.includes("="))
148
+ return argument.slice(0, argument.indexOf("=") + 1) + JSON.stringify(binaryMetadata(argument.slice(argument.indexOf("=") + 1)));
149
+ binaryValue = true;
150
+ }
151
+ }
152
+ // JSON-valued arguments need keyed filtering as well as token scanning.
153
+ if (/^[\[{]/.test(argument)) {
154
+ try {
155
+ return JSON.stringify(sanitizeDiagnosticContent(JSON.parse(argument)));
156
+ }
157
+ catch { /* ordinary text */ }
158
+ }
159
+ return sanitizeDiagnosticText(argument);
160
+ });
161
+ }
162
+ export function parseDiagnosticRecord(value) {
163
+ if (!value || typeof value !== "object" || Array.isArray(value))
164
+ throw new Error("Invalid diagnostic record");
165
+ const row = value;
166
+ if (row.version !== 1 || !diagnosticId(row.id) || !diagnosticId(row.invocation_id)
167
+ || !["cli", "mcp", "web", "worker"].includes(row.surface)
168
+ || !["invocation", "api", "worker", "protocol"].includes(row.layer)
169
+ || !["ok", "invalid_input", "refused", "platform_error", "network_error", "cancelled", "pending"].includes(row.outcome)
170
+ || !["complete", "partial"].includes(row.capture_state)
171
+ || typeof row.operation !== "string" || row.operation.length > 512
172
+ || !Number.isFinite(Date.parse(row.started_at))
173
+ || (row.ended_at !== null && !Number.isFinite(Date.parse(row.ended_at)))
174
+ || typeof row.identity_verified !== "boolean"
175
+ || (row.organization_id !== null && !diagnosticId(row.organization_id))
176
+ || (row.user_id !== null && typeof row.user_id !== "string")
177
+ || (row.parent_id !== null && !diagnosticId(row.parent_id))
178
+ || (row.status !== null && (!Number.isInteger(row.status) || row.status < 0 || row.status > 599))
179
+ || (row.error_code !== null && (typeof row.error_code !== "string" || row.error_code.length > 512))
180
+ || !row.metadata || typeof row.metadata !== "object" || Array.isArray(row.metadata))
181
+ throw new Error("Invalid diagnostic record");
182
+ return row;
183
+ }
184
+ export function diagnosticSummary(record) {
185
+ return {
186
+ diagnostic_id: record.id, invocation_id: record.invocation_id, parent_id: record.parent_id,
187
+ layer: record.layer, source: record.surface, operation: record.operation, outcome: record.outcome,
188
+ error_code: record.error_code, status: record.status, capture_state: record.capture_state,
189
+ identity_verified: record.identity_verified, client_version: record.client_version, trace_id: record.trace_id,
190
+ duration_ms: record.ended_at ? Math.max(0, Date.parse(record.ended_at) - Date.parse(record.started_at)) : null,
191
+ };
192
+ }
193
+ export function credentialSafeDiagnostic(record) {
194
+ const input = record.input;
195
+ if (Array.isArray(input)) {
196
+ const members = input.map(member => credentialSafeDiagnostic({ ...record, input: member }));
197
+ const withheld = (value) => Boolean(value && typeof value === "object" && "withheld" in value && value.withheld === "credential_bearing_operation");
198
+ return { ...record,
199
+ input: members.some(member => withheld(member.input)) ? { withheld: "credential_bearing_operation" } : input,
200
+ output: members.some(member => withheld(member.output)) ? { withheld: "credential_bearing_operation" } : record.output };
201
+ }
202
+ const params = input && typeof input === "object" && "params" in input ? input.params : null;
203
+ const toolName = params && typeof params.name === "string" ? params.name : record.operation;
204
+ const args = params?.arguments ?? input;
205
+ const selectedName = ["oxygen_capabilities_read", "oxygen_capabilities_call"].includes(toolName) && args && typeof args === "object" && "tool" in args
206
+ ? String(args.tool) : toolName;
207
+ return { ...record, input: credentialBearingOperation(selectedName, "input") ? { withheld: "credential_bearing_operation" } : record.input,
208
+ output: credentialBearingOperation(selectedName, "output") ? { withheld: "credential_bearing_operation" } : record.output };
209
+ }
210
+ /** Bound the diagnostic branch of a response tee without delaying its consumer. */
211
+ export async function readDiagnosticProtocolText(body) {
212
+ if (!body)
213
+ return null;
214
+ const reader = body.getReader();
215
+ const chunks = [];
216
+ let bytes = 0;
217
+ try {
218
+ while (true) {
219
+ const part = await reader.read();
220
+ if (part.done)
221
+ return Buffer.concat(chunks, bytes).toString("utf8");
222
+ bytes += part.value.byteLength;
223
+ if (bytes > DIAGNOSTIC_RECORD_MAX_BYTES) {
224
+ void reader.cancel().catch(() => undefined);
225
+ return null;
226
+ }
227
+ chunks.push(part.value);
228
+ }
229
+ }
230
+ catch {
231
+ return null;
232
+ }
233
+ finally {
234
+ reader.releaseLock();
235
+ }
236
+ }
237
+ /** Finite POST SSE is decoded before credential/binary filtering, like JSON. */
238
+ export function parseDiagnosticProtocolBody(text, contentType) {
239
+ if (text === null)
240
+ return null;
241
+ if (contentType?.includes("text/event-stream")) {
242
+ return text.split(/\r?\n\r?\n/).filter(Boolean).map(event => ({
243
+ event: event.split(/\r?\n/).find(line => line.startsWith("event:"))?.slice(6).trim() ?? null,
244
+ data: parseDiagnosticProtocolBody(event.split(/\r?\n/).filter(line => line.startsWith("data:")).map(line => line.slice(5).trimStart()).join("\n")),
245
+ }));
246
+ }
247
+ try {
248
+ return JSON.parse(text);
249
+ }
250
+ catch {
251
+ return text;
252
+ }
253
+ }
@@ -70,6 +70,14 @@ export type GeoAnswer = {
70
70
  prompt_revision?: number;
71
71
  topic?: string;
72
72
  };
73
+ export type GeoExportPagination = {
74
+ returned: number;
75
+ total: number;
76
+ next_cursor: string | null;
77
+ summary_scope: "all_filtered_answers";
78
+ };
79
+ /** Binary exports carry this metadata to web, CLI and MCP, even for CSV/PDF. */
80
+ export declare function readGeoExportPagination(headers: Pick<Headers, "get">): GeoExportPagination | undefined;
73
81
  export type GeoDocumentKind = "prompt" | "competitor" | "answer" | "evidence" | "opportunity" | "experiment" | "audit";
74
82
  export type GeoDocument = {
75
83
  id: string;
@@ -96,6 +104,7 @@ export type GeoRunItem = {
96
104
  error: string | null;
97
105
  attempt: number;
98
106
  terminal_before_dispatch?: boolean;
107
+ submission_rejected?: boolean;
99
108
  submission_cost_micros?: number | null;
100
109
  };
101
110
  export declare const GEO_PILOT_CAP_MICROS = 10000000;
@@ -110,10 +119,14 @@ export declare function analyzeGeoAnswer(text: string, brand: GeoCompetitor, com
110
119
  };
111
120
  /** Exact capture-time configuration, canonicalized for matched comparisons. */
112
121
  export declare function geoMeasurementKey(answer: Pick<GeoAnswer, "measurement">): string;
122
+ /** A scraper's question heading is not answer prose, including old stored captures. */
123
+ export declare function isGeoPromptEcho(text: string, prompt: string): boolean;
124
+ export declare function isGeoControlledAnswer(answer: GeoAnswer): boolean;
113
125
  export declare function summarizeGeoAnswers(answers: GeoAnswer[], brand: string, failedSamples?: number): {
114
126
  successful_samples: number;
115
127
  measurement_contexts: number;
116
128
  failed_samples: number;
129
+ invalid_answer_samples: number;
117
130
  imported_samples: number;
118
131
  no_overview_samples: number;
119
132
  mentioned_samples: number;
@@ -149,6 +162,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
149
162
  successful_samples: number;
150
163
  measurement_contexts: number;
151
164
  failed_samples: number;
165
+ invalid_answer_samples: number;
152
166
  imported_samples: number;
153
167
  no_overview_samples: number;
154
168
  mentioned_samples: number;
@@ -172,6 +186,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
172
186
  successful_samples: number;
173
187
  measurement_contexts: number;
174
188
  failed_samples: number;
189
+ invalid_answer_samples: number;
175
190
  imported_samples: number;
176
191
  no_overview_samples: number;
177
192
  mentioned_samples: number;
@@ -200,6 +215,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
200
215
  successful_samples: number;
201
216
  measurement_contexts: number;
202
217
  failed_samples: number;
218
+ invalid_answer_samples: number;
203
219
  imported_samples: number;
204
220
  no_overview_samples: number;
205
221
  mentioned_samples: number;
@@ -223,6 +239,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
223
239
  successful_samples: number;
224
240
  measurement_contexts: number;
225
241
  failed_samples: number;
242
+ invalid_answer_samples: number;
226
243
  imported_samples: number;
227
244
  no_overview_samples: number;
228
245
  mentioned_samples: number;
@@ -9,6 +9,24 @@ export const GEO_ENGINES = [
9
9
  "copilot",
10
10
  "claude",
11
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
+ }
12
30
  export const GEO_PILOT_CAP_MICROS = 10_000_000;
13
31
  /** The initial release gate is independent of NODE_ENV and protects shadow data. */
14
32
  export function isGeoEnabled(env = process.env) {
@@ -137,8 +155,18 @@ export function geoMeasurementKey(answer) {
137
155
  .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
138
156
  });
139
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
+ }
140
168
  export function summarizeGeoAnswers(answers, brand, failedSamples = 0) {
141
- const samples = answers.filter((a) => a.collection === "consumer" && a.outcome === "answer");
169
+ const samples = answers.filter(isGeoControlledAnswer);
142
170
  const measurementContexts = new Set(samples.map(geoMeasurementKey)).size;
143
171
  const comparable = measurementContexts <= 1;
144
172
  const brands = new Map();
@@ -170,6 +198,7 @@ export function summarizeGeoAnswers(answers, brand, failedSamples = 0) {
170
198
  successful_samples: samples.length,
171
199
  measurement_contexts: measurementContexts,
172
200
  failed_samples: failedSamples,
201
+ invalid_answer_samples: answers.filter((a) => a.collection === "consumer" && a.outcome === "answer" && !isGeoControlledAnswer(a)).length,
173
202
  imported_samples: answers.filter((a) => a.collection === "import").length,
174
203
  no_overview_samples: answers.filter((a) => a.outcome === "no_overview")
175
204
  .length,
@@ -279,8 +308,7 @@ export function parseGeoTranscript(input, redactions = []) {
279
308
  }
280
309
  /** Compare only common collection cohorts; imports and missing AIOs are excluded. */
281
310
  export function compareGeoExperiment(before, after, brand) {
282
- const eligible = (answers) => answers.filter((a) => a.collection === "consumer" &&
283
- a.outcome === "answer" &&
311
+ const eligible = (answers) => answers.filter((a) => isGeoControlledAnswer(a) &&
284
312
  a.measurement !== undefined);
285
313
  const key = (a) => JSON.stringify([
286
314
  a.prompt_id,
@@ -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",
@@ -15,6 +15,8 @@
15
15
  * set, so adding one is a deliberate edit there too.
16
16
  */
17
17
  export declare const PRODUCT_EVENTS: {
18
+ /** Internal diagnostic receipt, never counted as an additional customer action. */
19
+ readonly DIAGNOSTIC_CAPTURE: "diagnostic_capture";
18
20
  /** Acquisition. */
19
21
  readonly USER_SIGNED_UP: "user_signed_up";
20
22
  readonly WORKSPACE_CREATED: "workspace_created";
@@ -15,6 +15,8 @@
15
15
  * set, so adding one is a deliberate edit there too.
16
16
  */
17
17
  export const PRODUCT_EVENTS = {
18
+ /** Internal diagnostic receipt, never counted as an additional customer action. */
19
+ DIAGNOSTIC_CAPTURE: "diagnostic_capture",
18
20
  /** Acquisition. */
19
21
  USER_SIGNED_UP: "user_signed_up",
20
22
  WORKSPACE_CREATED: "workspace_created",
@@ -1,2 +1,3 @@
1
1
  export declare const OXYGEN_VERSION: string;
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.922.14";
3
+ export declare const GEO_EXPORT_MINIMUM_CLI_VERSION = "1.1013.0";
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1012.2";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1013.15";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1012.2";
2
+ export const OXYGEN_BUILD_VERSION = "1.1013.15";
@@ -50,3 +50,6 @@ export const OXYGEN_VERSION = OXYGEN_BUILD_VERSION;
50
50
  // highest CLI version published on npm for at least 30 days. It never lowers
51
51
  // the floor. The source change lands on dev before the next production release.
52
52
  export const OXYGEN_MINIMUM_CLI_VERSION = "1.922.14";
53
+ // The internal GEO export first gains required pagination in this dev version.
54
+ // Earlier pilot clients would save a partial file without its continuation.
55
+ export const GEO_EXPORT_MINIMUM_CLI_VERSION = "1.1013.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.1012.2",
3
+ "version": "1.1013.15",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",