@oxygen-agent/cli 1.1012.2 → 1.1013.27

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 (52) 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 +16 -4
  12. package/node_modules/@oxygen/cli-ugc/dist/commands.js +14 -2
  13. package/node_modules/@oxygen/shared/dist/capability-discovery.js +4 -4
  14. package/node_modules/@oxygen/shared/dist/column-operation.d.ts +5 -0
  15. package/node_modules/@oxygen/shared/dist/column-operation.js +6 -0
  16. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -1
  17. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -0
  18. package/node_modules/@oxygen/shared/dist/contracts/geo.js +30 -8
  19. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +1 -0
  20. package/node_modules/@oxygen/shared/dist/diagnostic-context.d.ts +3 -0
  21. package/node_modules/@oxygen/shared/dist/diagnostic-context.js +17 -0
  22. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +4 -0
  23. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +12 -0
  24. package/node_modules/@oxygen/shared/dist/diagnostic-lock.d.ts +2 -0
  25. package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +60 -0
  26. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.d.ts +6 -0
  27. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.js +22 -0
  28. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.d.ts +2 -0
  29. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +39 -0
  30. package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +31 -0
  31. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +233 -0
  32. package/node_modules/@oxygen/shared/dist/diagnostics.d.ts +49 -0
  33. package/node_modules/@oxygen/shared/dist/diagnostics.js +253 -0
  34. package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +7 -0
  35. package/node_modules/@oxygen/shared/dist/future-signup-events.js +11 -0
  36. package/node_modules/@oxygen/shared/dist/geo.d.ts +17 -0
  37. package/node_modules/@oxygen/shared/dist/geo.js +31 -3
  38. package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +7 -0
  39. package/node_modules/@oxygen/shared/dist/knowledge-constants.js +9 -0
  40. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.d.ts +19 -0
  41. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.js +25 -0
  42. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
  43. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +4 -0
  44. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +9 -0
  45. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +9 -0
  46. package/node_modules/@oxygen/shared/dist/ugc.d.ts +9 -0
  47. package/node_modules/@oxygen/shared/dist/ugc.js +12 -0
  48. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -0
  49. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  50. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  51. package/node_modules/@oxygen/shared/dist/version.js +3 -0
  52. 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
+ }
@@ -13,6 +13,13 @@ export declare const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION: "futur
13
13
  export declare const FUTURE_SIGNUP_CRM_DESTINATION = "oxygen_company_crm";
14
14
  export declare const FUTURE_SIGNUP_ACTIVATION_COMMANDS: readonly ["agent create", "crm activity log", "crm assert", "crm object add-attr", "crm object create", "crm objects add-attr", "crm objects create", "crm relationships define", "crm relationships upsert", "knowledge page upsert", "publishing posts create", "sequences create", "tables create", "tables duplicate", "tables import-csv", "tables import-file", "tables import-staged", "tables import-url", "tables insert", "tables relate", "tables update", "tables upsert", "workflows apply"];
15
15
  export declare function isFutureSignupActivationCommand(command: string): boolean;
16
+ /**
17
+ * The browser resource routes (`/api/cli/tables/{table}/rows…`) report their own command
18
+ * names, and the web grid and record panel write through them. Each is the same creation as
19
+ * its flat twin in the set above, so it counts as that: in the first-record marker and in the
20
+ * staff KPI board's web activation.
21
+ */
22
+ export declare const FUTURE_SIGNUP_ACTIVATION_COMMAND_ALIASES: Readonly<Record<string, string>>;
16
23
  export declare function futureSignupJourneyId(clerkUserId: string): string;
17
24
  export declare function futureSignupFactId(clerkUserId: string): string;
18
25
  export declare function futureSignupOperationFactId(operationEventId: string): string;
@@ -42,6 +42,17 @@ const ACTIVATION_COMMAND_SET = new Set(FUTURE_SIGNUP_ACTIVATION_COMMANDS);
42
42
  export function isFutureSignupActivationCommand(command) {
43
43
  return ACTIVATION_COMMAND_SET.has(command.trim().toLowerCase());
44
44
  }
45
+ /**
46
+ * The browser resource routes (`/api/cli/tables/{table}/rows…`) report their own command
47
+ * names, and the web grid and record panel write through them. Each is the same creation as
48
+ * its flat twin in the set above, so it counts as that: in the first-record marker and in the
49
+ * staff KPI board's web activation.
50
+ */
51
+ export const FUTURE_SIGNUP_ACTIVATION_COMMAND_ALIASES = {
52
+ "tables rows insert": "tables insert",
53
+ "tables rows edit": "tables update",
54
+ "tables rows relationship-set": "tables relate",
55
+ };
45
56
  export function futureSignupJourneyId(clerkUserId) {
46
57
  return `future_signup:v1:clerk_user:${requiredId(clerkUserId, "clerkUserId")}`;
47
58
  }
@@ -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,
@@ -41,3 +41,10 @@ export declare const KNOWLEDGE_STUB_MARKER = "Stub \u2014 fill me.";
41
41
  export declare const KNOWLEDGE_BOOTSTRAP_COMMAND = "oxygen knowledge bootstrap";
42
42
  export declare const KNOWLEDGE_BOOTSTRAP_AVAILABLE = true;
43
43
  export declare const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS: readonly ["oxygen_page", "id", "slug", "type", "title", "status", "tags", "canonical", "summary", "revision", "updated_at", "web_url"];
44
+ /**
45
+ * A Knowledge folder is a skill when it holds the skill's entry file, named
46
+ * SKILL (the Agent Skills SKILL.md convention); its other files are the
47
+ * skill's supporting files. Shared by the program file picker and the reads
48
+ * that load a creator's program files.
49
+ */
50
+ export declare function isKnowledgeSkillEntryTitle(title: string): boolean;
@@ -159,3 +159,12 @@ export const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS = [
159
159
  "updated_at",
160
160
  "web_url",
161
161
  ];
162
+ /**
163
+ * A Knowledge folder is a skill when it holds the skill's entry file, named
164
+ * SKILL (the Agent Skills SKILL.md convention); its other files are the
165
+ * skill's supporting files. Shared by the program file picker and the reads
166
+ * that load a creator's program files.
167
+ */
168
+ export function isKnowledgeSkillEntryTitle(title) {
169
+ return /^skill(\.md)?$/i.test(title.trim());
170
+ }
@@ -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";
@@ -42,6 +44,13 @@ export declare const PRODUCT_EVENTS: {
42
44
  readonly FIRST_PLAY_STARTED: "first_play_started";
43
45
  readonly FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed";
44
46
  readonly FIRST_PLAY_COMPLETED: "first_play_completed";
47
+ /**
48
+ * The getting-started checklist (2026-10-09): one event per workspace and step, the
49
+ * first time any surface's read sees it done, gated by a stamp on the organization
50
+ * (apps/web `lib/activation/getting-started.ts`). `step` is first_leads,
51
+ * first_sequence, first_account or first_launch.
52
+ */
53
+ readonly GETTING_STARTED_STEP_COMPLETED: "getting_started_step_completed";
45
54
  /**
46
55
  * Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
47
56
  * second highest-volume event; it was REPLACED (not joined) by the reserved
@@ -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",
@@ -42,6 +44,13 @@ export const PRODUCT_EVENTS = {
42
44
  FIRST_PLAY_STARTED: "first_play_started",
43
45
  FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed",
44
46
  FIRST_PLAY_COMPLETED: "first_play_completed",
47
+ /**
48
+ * The getting-started checklist (2026-10-09): one event per workspace and step, the
49
+ * first time any surface's read sees it done, gated by a stamp on the organization
50
+ * (apps/web `lib/activation/getting-started.ts`). `step` is first_leads,
51
+ * first_sequence, first_account or first_launch.
52
+ */
53
+ GETTING_STARTED_STEP_COMPLETED: "getting_started_step_completed",
45
54
  /**
46
55
  * Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
47
56
  * second highest-volume event; it was REPLACED (not joined) by the reserved
@@ -14,6 +14,13 @@ export declare const UGC_POSTING_TARGET_PERIODS: readonly ["week", "month", "tot
14
14
  * operators either trimmed it or left the program without one).
15
15
  */
16
16
  export declare const UGC_PROGRAM_BRIEF_MAX_CHARS = 4000;
17
+ /**
18
+ * A creator's live, read-only handle on one file a program shares:
19
+ * `ugc:<programId>:<page id or slug>`. Every surface that lists or opens a
20
+ * shared file builds and reads it here, so web, worker and agents agree.
21
+ */
22
+ export declare function ugcKnowledgeReference(programId: string, key: string): string;
23
+ export declare function isUgcKnowledgeReference(value: string): boolean;
17
24
  /** The program's target as its API shape, or null when none is set. */
18
25
  export declare function ugcPostingTarget(program: {
19
26
  postingTargetCount?: number | null | undefined;
@@ -25,6 +32,8 @@ export type UgcProgram = {
25
32
  productUrl: string;
26
33
  description: string;
27
34
  knowledgePageSlugs: string[];
35
+ /** Folders shared live with creators (skills included); absent before migration 0482. */
36
+ knowledgeFolderIds?: string[];
28
37
  peerEngagementEnabled?: boolean;
29
38
  peerEngagementVersion?: number;
30
39
  peerEngagementEffectiveAt?: Date | null;
@@ -6,6 +6,18 @@ export const UGC_POSTING_TARGET_PERIODS = ["week", "month", "total"];
6
6
  * operators either trimmed it or left the program without one).
7
7
  */
8
8
  export const UGC_PROGRAM_BRIEF_MAX_CHARS = 4000;
9
+ const UGC_KNOWLEDGE_REFERENCE_PREFIX = "ugc:";
10
+ /**
11
+ * A creator's live, read-only handle on one file a program shares:
12
+ * `ugc:<programId>:<page id or slug>`. Every surface that lists or opens a
13
+ * shared file builds and reads it here, so web, worker and agents agree.
14
+ */
15
+ export function ugcKnowledgeReference(programId, key) {
16
+ return `${UGC_KNOWLEDGE_REFERENCE_PREFIX}${programId}:${key}`;
17
+ }
18
+ export function isUgcKnowledgeReference(value) {
19
+ return value.startsWith(UGC_KNOWLEDGE_REFERENCE_PREFIX);
20
+ }
9
21
  /** The program's target as its API shape, or null when none is set. */
10
22
  export function ugcPostingTarget(program) {
11
23
  return program.postingTargetCount && program.postingTargetPeriod
@@ -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.27";
@@ -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.27";
@@ -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.27",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",