@oxygen-agent/cli 1.1011.1091 → 1.1013.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.d.ts +3 -1
  3. package/dist/command-manifest.js +11 -0
  4. package/dist/customer-operation-commands.js +7 -3
  5. package/dist/diagnostic-upload-main.d.ts +1 -0
  6. package/dist/diagnostic-upload-main.js +2 -0
  7. package/dist/diagnostic-uploader.d.ts +3 -0
  8. package/dist/diagnostic-uploader.js +68 -0
  9. package/dist/diagnostics.d.ts +22 -0
  10. package/dist/diagnostics.js +219 -0
  11. package/dist/http-client.d.ts +7 -0
  12. package/dist/http-client.js +12 -2
  13. package/dist/index.js +72 -34
  14. package/dist/transcript.js +1 -1
  15. package/node_modules/@oxygen/formula/dist/expression.js +16 -0
  16. package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +8 -0
  17. package/node_modules/@oxygen/shared/dist/attempt-provenance.js +15 -0
  18. package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
  19. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +2 -1
  20. package/node_modules/@oxygen/shared/dist/capability-discovery.js +28 -4
  21. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +3 -3
  22. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +3 -3
  23. package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/geo.js +160 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +1 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  27. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +10 -4
  29. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
  30. package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
  31. package/node_modules/@oxygen/shared/dist/diagnostic-context.d.ts +3 -0
  32. package/node_modules/@oxygen/shared/dist/diagnostic-context.js +17 -0
  33. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +4 -0
  34. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +12 -0
  35. package/node_modules/@oxygen/shared/dist/diagnostic-lock.d.ts +2 -0
  36. package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +60 -0
  37. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.d.ts +6 -0
  38. package/node_modules/@oxygen/shared/dist/diagnostic-receipt.js +22 -0
  39. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.d.ts +2 -0
  40. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +39 -0
  41. package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +31 -0
  42. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +233 -0
  43. package/node_modules/@oxygen/shared/dist/diagnostics.d.ts +49 -0
  44. package/node_modules/@oxygen/shared/dist/diagnostics.js +253 -0
  45. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
  46. package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
  47. package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
  48. package/node_modules/@oxygen/shared/dist/geo.d.ts +264 -0
  49. package/node_modules/@oxygen/shared/dist/geo.js +342 -0
  50. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
  51. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
  52. package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
  53. package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
  54. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.d.ts +19 -0
  55. package/node_modules/@oxygen/shared/dist/onboarding-first-leads.js +25 -0
  56. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
  57. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +4 -0
  58. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
  59. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
  60. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
  61. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
  62. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +2 -0
  63. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +2 -0
  64. package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
  65. package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
  66. package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
  67. package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
  68. package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
  69. package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
  70. package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
  71. package/node_modules/@oxygen/shared/dist/version.d.ts +2 -1
  72. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  73. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  74. package/node_modules/@oxygen/shared/dist/version.js +4 -1
  75. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +46 -0
  76. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +51 -1
  77. package/node_modules/@oxygen/shared/package.json +10 -0
  78. package/package.json +1 -1
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.1011.1091
37
+ Version: 1.1013.15
@@ -17,6 +17,7 @@ export type CommandManifestEntry = {
17
17
  flags: CommandManifestFlag[];
18
18
  spends_credits: boolean;
19
19
  mutates: boolean;
20
+ destructive: boolean;
20
21
  preview_by_default: boolean;
21
22
  json_supported: boolean;
22
23
  hidden: boolean;
@@ -33,10 +34,11 @@ export type CommandSearchResult = {
33
34
  total_matches: number;
34
35
  returned: number;
35
36
  route: Record<string, unknown> | null;
36
- commands: Array<Pick<CommandManifestEntry, "name" | "group" | "description" | "spends_credits" | "mutates" | "json_supported" | "hidden"> & {
37
+ commands: Array<Pick<CommandManifestEntry, "name" | "group" | "description" | "spends_credits" | "mutates" | "destructive" | "json_supported" | "hidden"> & {
37
38
  score: number;
38
39
  }>;
39
40
  hydrate: string;
41
+ next_step?: string;
40
42
  };
41
43
  export declare function buildCommandManifest(program: Command, binaryName: string): CommandManifest;
42
44
  /** Bounded natural-language projection over the complete local manifest. */
@@ -108,6 +108,13 @@ const MUTATING_COMMANDS = new Set([
108
108
  // credits. The exact exceptions disclose writes without inventing spend.
109
109
  "billing balance",
110
110
  "billing seats",
111
+ // The ledger audit reads that same balance first, with the same conditional writes.
112
+ "billing audit",
113
+ // Creates an approval or Checkout link; a first plan's Checkout can start the
114
+ // first-month offer window. The plan changes only after an admin confirms.
115
+ "billing change",
116
+ // --approved records a domain's renewal exit, or with --revoke resumes renewals.
117
+ "managed-inboxes exit",
111
118
  // A default preview is free; approval restores provider seats and billing.
112
119
  "managed-inboxes recover",
113
120
  "mailboxes warmup status",
@@ -215,6 +222,7 @@ const READ_ONLY_COMMANDS = new Set([
215
222
  // invocation still previews, so machine-readable discovery must not imply that
216
223
  // calling the command without its confirmation flag writes anything.
217
224
  const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
225
+ "mailboxes warmup enable",
218
226
  "mailboxes warmup google",
219
227
  "deliverability placement-test run",
220
228
  "deliverability placement-test send",
@@ -300,11 +308,13 @@ export function searchCommandManifest(manifest, query, requestedLimit = DEFAULT_
300
308
  description: command.description,
301
309
  spends_credits: command.spends_credits,
302
310
  mutates: command.mutates,
311
+ destructive: command.destructive,
303
312
  json_supported: command.json_supported,
304
313
  hidden: command.hidden,
305
314
  score,
306
315
  })),
307
316
  hydrate: `${manifest.binary} commands get <exact-command> --json`,
317
+ ...(matches.length ? {} : { next_step: `No command matched. Describe the outcome with ${manifest.binary} capabilities search, or inspect ${manifest.binary} commands --json.` }),
308
318
  };
309
319
  }
310
320
  /** Exact second hop after search; avoids serializing the full command grammar. */
@@ -426,6 +436,7 @@ function toManifestEntry(command, path) {
426
436
  && (MUTATING_COMMANDS.has(commandName)
427
437
  || MUTATING_VERBS.has(leafVerb)
428
438
  || MUTATING_VERBS.has(verbPrefix))),
439
+ destructive: operations.some((spec) => spec.mcp.annotations?.destructiveHint === true),
429
440
  preview_by_default: flagStrings.some((flags) => flags.includes("--live")) ||
430
441
  PREVIEW_BY_DEFAULT_COMMANDS.has(commandName),
431
442
  json_supported: flagStrings.some((flags) => flags.includes("--json")),
@@ -34,7 +34,7 @@ const GROUP_PURPOSES = {
34
34
  const OPERATION_FLAGS = {
35
35
  "tables rows remove-selection": {
36
36
  table: { help: "Table id or slug." },
37
- row_ids: { help: "Comma-separated exact row IDs. Choose this or --selection-json." },
37
+ row_ids: { help: "Comma-separated exact row IDs. Choose this, --filter-tree-json or --selection-json." },
38
38
  },
39
39
  "tables favorite": { favorite: { negateHelp: "Unstar the selected Table or folder (favorite: false)." } },
40
40
  "tables favorites": { details: { negateHelp: "Return stored star keys without names or links (details: false)." } },
@@ -116,7 +116,11 @@ export function registerCustomerOperationCommands(program, handle) {
116
116
  if (spec.binary) {
117
117
  if (spec.binary.stream)
118
118
  return requestOxygen(request.path, { method: request.method, binaryOutputPath: resolve(String(options.output)) });
119
- const bytes = await requestOxygen(request.path, { method: request.method, binaryMaxBytes: spec.binary.maxBytes });
119
+ const download = await requestOxygen(request.path, { method: request.method, binaryMaxBytes: spec.binary.maxBytes, ...(spec.binary.paginated ? { binaryMetadata: true } : {}) });
120
+ const bytes = download instanceof Uint8Array ? download : download.bytes;
121
+ const pagination = download instanceof Uint8Array ? undefined : download.pagination;
122
+ if (spec.binary.paginated && !pagination)
123
+ throw new OxygenError("invalid_response", "The export did not include its evidence pagination. Retry the same read.");
120
124
  const output = resolve(String(options.output));
121
125
  try {
122
126
  await writeFile(output, bytes, { flag: "wx" });
@@ -127,7 +131,7 @@ export function registerCustomerOperationCommands(program, handle) {
127
131
  }
128
132
  throw error;
129
133
  }
130
- return { path: output, bytes: bytes.byteLength };
134
+ return { path: output, bytes: bytes.byteLength, ...(pagination ? { pagination } : {}) };
131
135
  }
132
136
  if (spec.multipart) {
133
137
  let formData;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { uploadCliDiagnostics } from "./diagnostic-uploader.js";
2
+ await uploadCliDiagnostics().catch(() => undefined);
@@ -0,0 +1,3 @@
1
+ import { type StoredCredentials } from "./credentials.js";
2
+ /** Dedicated detached process: credential values never enter argv or the spool. */
3
+ export declare function uploadCliDiagnostics(directory?: string, credentials?: StoredCredentials): Promise<number>;
@@ -0,0 +1,68 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { acquireDiagnosticLock } from "@oxygen/shared/diagnostic-lock";
4
+ import { join } from "node:path";
5
+ import { gunzipSync, gzipSync } from "node:zlib";
6
+ import { DIAGNOSTIC_RECORD_MAX_BYTES, DIAGNOSTIC_UPLOAD_CHUNK_BYTES, parseDiagnosticRecord } from "@oxygen/shared/diagnostics";
7
+ import { loadCredentialsWithEndpoint } from "./credentials.js";
8
+ import { cliDiagnosticDirectory, credentialFingerprint } from "./diagnostics.js";
9
+ import { requestOxygen } from "./http-client.js";
10
+ /** Dedicated detached process: credential values never enter argv or the spool. */
11
+ export async function uploadCliDiagnostics(directory = cliDiagnosticDirectory(), credentials) {
12
+ if (process.env.OXYGEN_DIAGNOSTICS_ENABLED !== "1")
13
+ return 0;
14
+ const loaded = credentials ?? (await loadCredentialsWithEndpoint()).credentials;
15
+ if (!loaded)
16
+ return 0;
17
+ const lockPath = join(directory, ".upload.lock");
18
+ const release = await acquireDiagnosticLock(lockPath);
19
+ if (!release)
20
+ return 0;
21
+ let uploaded = 0;
22
+ try {
23
+ for (const name of (await readdir(directory)).filter((name) => /^[0-9a-f-]+\.json\.gz$/.test(name)).slice(0, 20)) {
24
+ const path = join(directory, name);
25
+ try {
26
+ const entry = JSON.parse(gunzipSync(await readFile(path), { maxOutputLength: DIAGNOSTIC_RECORD_MAX_BYTES }).toString("utf8"));
27
+ parseDiagnosticRecord(entry.record);
28
+ if (Date.now() - Date.parse(entry.record.started_at) >= 72 * 60 * 60 * 1000)
29
+ continue;
30
+ // Never move an old invocation to a different endpoint, user or workspace.
31
+ if (new URL(entry.scope.api_url).origin !== new URL(loaded.apiUrl).origin
32
+ || (entry.scope.fingerprint && entry.scope.fingerprint !== credentialFingerprint(loaded)))
33
+ continue;
34
+ const bytes = gzipSync(Buffer.from(JSON.stringify(entry)));
35
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
36
+ const total = Math.ceil(bytes.length / DIAGNOSTIC_UPLOAD_CHUNK_BYTES);
37
+ const selectedOrganization = entry.scope.organization ?? undefined;
38
+ const progressPath = path + ".upload.json";
39
+ const progress = JSON.parse(await readFile(progressPath, "utf8").catch(() => "null"));
40
+ // Staging expires after one day. Restart from zero after that boundary.
41
+ const next = progress?.sha256 === sha256 && Date.now() - progress.updated_at < 23 * 3_600_000
42
+ && Number.isInteger(progress.next) && progress.next >= 0 && progress.next <= total ? progress.next : 0;
43
+ for (let index = next; index < total; index++) {
44
+ await requestOxygen("/api/cli/observability/diagnostics", { method: "POST", credentials: loaded,
45
+ ...(selectedOrganization ? { selectedOrganization } : {}), timeoutMs: 15_000,
46
+ body: { id: entry.record.id, index, total, sha256,
47
+ chunk: bytes.subarray(index * DIAGNOSTIC_UPLOAD_CHUNK_BYTES, (index + 1) * DIAGNOSTIC_UPLOAD_CHUNK_BYTES).toString("base64") },
48
+ });
49
+ const temporary = progressPath + ".tmp";
50
+ await writeFile(temporary, JSON.stringify({ sha256, next: index + 1, updated_at: Date.now() }), { mode: 0o600 });
51
+ await rename(temporary, progressPath);
52
+ }
53
+ const receipt = await requestOxygen("/api/cli/observability/diagnostics", { method: "POST", credentials: loaded,
54
+ ...(selectedOrganization ? { selectedOrganization } : {}), timeoutMs: 15_000, body: { id: entry.record.id, total, sha256, complete: true } });
55
+ if (receipt.accepted !== true || receipt.complete !== true || receipt.diagnostic_id !== entry.record.id)
56
+ continue;
57
+ await unlink(path);
58
+ await unlink(progressPath).catch(() => undefined);
59
+ uploaded++;
60
+ }
61
+ catch { /* leave the original for a later invocation; never replay product actions */ }
62
+ }
63
+ }
64
+ finally {
65
+ await release();
66
+ }
67
+ return uploaded;
68
+ }
@@ -0,0 +1,22 @@
1
+ import { type DiagnosticRecord } from "@oxygen/shared/diagnostics";
2
+ import { type StoredCredentials } from "./credentials.js";
3
+ export type CliDiagnosticScope = {
4
+ api_url: string;
5
+ fingerprint: string | null;
6
+ organization: string | null;
7
+ user: string | null;
8
+ };
9
+ export type CliDiagnosticSpoolEntry = {
10
+ scope: CliDiagnosticScope;
11
+ record: DiagnosticRecord;
12
+ };
13
+ export declare function cliDiagnosticDirectory(): string;
14
+ export declare function credentialFingerprint(credentials: StoredCredentials): string;
15
+ export declare function beginCliDiagnostics(argv?: string[]): void;
16
+ export declare function setCliDiagnosticOperation(command: string): void;
17
+ export declare function cliDiagnosticInvocation(): string | undefined;
18
+ export declare function cliDiagnosticHeaders(): Record<string, string>;
19
+ export declare function recordCliDiagnosticRequest(input: unknown): void;
20
+ export declare function recordCliDiagnosticFailure(error: unknown): void;
21
+ export declare function finishCliDiagnostics(exitCode: number): Promise<string | null>;
22
+ export declare function writeCliDiagnosticSpool(directory: string, entry: CliDiagnosticSpoolEntry): Promise<string | null>;
@@ -0,0 +1,219 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, open, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { acquireDiagnosticLock } from "@oxygen/shared/diagnostic-lock";
8
+ import { StringDecoder } from "node:string_decoder";
9
+ import { gzipSync } from "node:zlib";
10
+ import { createDiagnosticRecord, credentialSafeDiagnostic, diagnosticId, DIAGNOSTIC_INVOCATION_HEADER, DIAGNOSTIC_MAX_BYTES, DIAGNOSTIC_RECORD_MAX_BYTES, diagnosticOutcome, sanitizeDiagnosticArgv, sanitizeDiagnosticContent } from "@oxygen/shared/diagnostics";
11
+ import { OXYGEN_VERSION } from "@oxygen/shared/version";
12
+ import { loadCredentialsWithEndpoint } from "./credentials.js";
13
+ let active;
14
+ export function cliDiagnosticDirectory() {
15
+ return join(process.env.OXYGEN_CONFIG_DIR?.trim() || (process.platform === "win32"
16
+ ? join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "oxygen")
17
+ : join(homedir(), ".config", "oxygen")), "diagnostics", "v1");
18
+ }
19
+ export function credentialFingerprint(credentials) {
20
+ return createHash("sha256").update(credentials.token).digest("hex");
21
+ }
22
+ export function beginCliDiagnostics(argv = process.argv.slice(2)) {
23
+ if (active || process.env.OXYGEN_DIAGNOSTICS_ENABLED !== "1")
24
+ return;
25
+ const invocation = createDiagnosticRecord({ surface: "cli", operation: "cli parse",
26
+ client_version: OXYGEN_VERSION, input: { argv: sanitizeDiagnosticArgv(argv) },
27
+ trace_id: randomUUID().replaceAll("-", ""),
28
+ });
29
+ invocation.trace_id = invocation.invocation_id.replaceAll("-", "");
30
+ invocation.traceparent = `00-${invocation.trace_id}-${randomUUID().replaceAll("-", "").slice(0, 16)}-01`;
31
+ const credentialEnv = diagnosticCredentialEnvironment(argv);
32
+ const context = { record: invocation, credentialEnv, scope: loadCredentialsWithEndpoint(credentialEnv).catch(() => null),
33
+ organizationRef: credentialEnv.OXYGEN_ORG?.trim() || null, signalHandlers: [],
34
+ requests: [], bytes: 0, droppedBytes: 0, stdout: [], stderr: [], restore: () => undefined };
35
+ active = context;
36
+ const restorers = [];
37
+ for (const [stream, output] of [[process.stdout, context.stdout], [process.stderr, context.stderr]]) {
38
+ const original = stream.write;
39
+ const decoder = new StringDecoder("utf8");
40
+ const wrapped = function (...args) {
41
+ try {
42
+ const chunk = args[0];
43
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
44
+ if (context.bytes + bytes.length <= DIAGNOSTIC_RECORD_MAX_BYTES) {
45
+ context.bytes += bytes.length;
46
+ output.push(decoder.write(bytes));
47
+ }
48
+ else
49
+ context.droppedBytes += bytes.length;
50
+ }
51
+ catch {
52
+ context.droppedBytes++;
53
+ }
54
+ return original.apply(stream, args);
55
+ };
56
+ stream.write = wrapped;
57
+ restorers.push(() => { output.push(decoder.end()); stream.write = original; });
58
+ }
59
+ context.restore = () => { for (const restore of restorers)
60
+ restore(); };
61
+ for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143]]) {
62
+ const handler = () => {
63
+ recordCliDiagnosticFailure({ code: "interrupted" });
64
+ void finishCliDiagnostics(code).finally(() => process.exit(code));
65
+ };
66
+ process.once(signal, handler);
67
+ context.signalHandlers.push(() => process.removeListener(signal, handler));
68
+ }
69
+ }
70
+ export function setCliDiagnosticOperation(command) {
71
+ if (active)
72
+ active.record.operation = command.slice(0, 512);
73
+ }
74
+ export function cliDiagnosticInvocation() { return active?.record.invocation_id; }
75
+ export function cliDiagnosticHeaders() {
76
+ return active ? { [DIAGNOSTIC_INVOCATION_HEADER]: active.record.invocation_id, traceparent: active.record.traceparent } : {};
77
+ }
78
+ export function recordCliDiagnosticRequest(input) {
79
+ if (!active)
80
+ return;
81
+ try {
82
+ const sanitized = sanitizeDiagnosticContent(input);
83
+ const size = Buffer.byteLength(JSON.stringify(sanitized));
84
+ if (active.bytes + size <= DIAGNOSTIC_RECORD_MAX_BYTES) {
85
+ active.requests.push(sanitized);
86
+ active.bytes += size;
87
+ }
88
+ else
89
+ active.droppedBytes += size;
90
+ }
91
+ catch {
92
+ active.droppedBytes++;
93
+ }
94
+ }
95
+ export function recordCliDiagnosticFailure(error) {
96
+ if (!active)
97
+ return;
98
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "unexpected_error";
99
+ active.record.error_code = code;
100
+ active.record.outcome = diagnosticOutcome(false, null, code);
101
+ recordCliDiagnosticRequest({ error });
102
+ }
103
+ export async function finishCliDiagnostics(exitCode) {
104
+ const context = active;
105
+ if (!context)
106
+ return null;
107
+ context.restore();
108
+ for (const remove of context.signalHandlers)
109
+ remove();
110
+ active = undefined;
111
+ try {
112
+ const loaded = await context.scope;
113
+ const credentials = loaded?.credentials;
114
+ const storedOrganization = credentials?.activeOrganization ?? credentials?.identity?.organization;
115
+ const organization = !context.organizationRef ? storedOrganization?.id ?? null
116
+ : context.organizationRef === storedOrganization?.id || context.organizationRef === storedOrganization?.slug
117
+ ? storedOrganization.id : diagnosticId(context.organizationRef);
118
+ const user = credentials?.identity?.user.id ?? null;
119
+ const record = context.record;
120
+ record.ended_at = new Date().toISOString();
121
+ if (record.outcome === "pending")
122
+ record.outcome = exitCode === 0 ? "ok" : "invalid_input";
123
+ record.status = exitCode;
124
+ record.organization_id = organization;
125
+ record.user_id = user;
126
+ record.capture_state = context.droppedBytes ? "partial" : "complete";
127
+ record.output = { stdout: context.stdout.join(""), stderr: context.stderr.join("") };
128
+ record.metadata = { requests: context.requests, dropped_bytes: context.droppedBytes, client_reported: true,
129
+ original_organization_ref: context.organizationRef };
130
+ const entry = { scope: { api_url: loaded?.endpoint.apiUrl ?? process.env.OXYGEN_API_URL?.trim() ?? "https://oxygen-agent.com",
131
+ fingerprint: credentials ? credentialFingerprint(credentials) : null, organization: organization ?? context.organizationRef, user },
132
+ record: sanitizeDiagnosticContent(credentialSafeDiagnostic(record), [credentials?.token ?? ""]) };
133
+ const directory = cliDiagnosticDirectory();
134
+ const path = await writeCliDiagnosticSpool(directory, entry);
135
+ if (path && process.env.OXYGEN_DIAGNOSTICS_UPLOAD_DISABLED !== "1") {
136
+ const child = spawn(process.execPath, [fileURLToPath(new URL("./diagnostic-upload-main.js", import.meta.url))], { detached: true, stdio: "ignore", env: context.credentialEnv });
137
+ child.on("error", () => undefined);
138
+ child.unref();
139
+ }
140
+ return path;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ export async function writeCliDiagnosticSpool(directory, entry) {
147
+ await mkdir(directory, { recursive: true, mode: 0o700 });
148
+ const lockPath = join(directory, ".admission.lock");
149
+ const release = await acquireDiagnosticLock(lockPath);
150
+ if (!release)
151
+ return null;
152
+ try {
153
+ let used = 0;
154
+ let dropped = 0;
155
+ for (const name of await readdir(directory)) {
156
+ if (!/^[0-9a-f-]+\.json\.gz$/.test(name) && !/^[0-9a-f-]+\.json\.gz\.[0-9a-f-]+\.tmp$/.test(name))
157
+ continue;
158
+ const path = join(directory, name);
159
+ const info = await stat(path).catch(() => null);
160
+ if (!info)
161
+ continue;
162
+ if (Date.now() - info.mtimeMs >= 72 * 60 * 60 * 1000) {
163
+ await unlink(path);
164
+ dropped += info.size;
165
+ }
166
+ else
167
+ used += info.size;
168
+ }
169
+ const lossPath = join(directory, "loss.json");
170
+ const previous = JSON.parse(await readFile(lossPath, "utf8").catch(() => "{\"bytes\":0}"));
171
+ let bytes = Buffer.from(JSON.stringify({ ...entry, record: sanitizeDiagnosticContent({ ...entry.record,
172
+ metadata: { ...entry.record.metadata, prior_spool_lost_bytes: previous.bytes + dropped } }) }));
173
+ if (bytes.length > DIAGNOSTIC_RECORD_MAX_BYTES) {
174
+ bytes = Buffer.from(JSON.stringify({ ...entry, record: { ...entry.record, input: null, output: null,
175
+ capture_state: "partial", metadata: { capture_reason: "oversized_content", dropped_bytes: bytes.length } } }));
176
+ }
177
+ const compressed = gzipSync(bytes);
178
+ if (compressed.length + used > DIAGNOSTIC_MAX_BYTES)
179
+ dropped += compressed.length;
180
+ if (dropped) {
181
+ const loss = await open(lossPath, "w", 0o600);
182
+ try {
183
+ await loss.writeFile(JSON.stringify({ bytes: previous.bytes + dropped }));
184
+ await loss.sync();
185
+ }
186
+ finally {
187
+ await loss.close();
188
+ }
189
+ }
190
+ if (compressed.length + used > DIAGNOSTIC_MAX_BYTES)
191
+ return null;
192
+ const path = join(directory, `${entry.record.id}.json.gz`);
193
+ const temporary = path + `.${randomUUID()}.tmp`;
194
+ const handle = await open(temporary, "wx", 0o600);
195
+ try {
196
+ await handle.writeFile(compressed);
197
+ await handle.sync();
198
+ }
199
+ finally {
200
+ await handle.close();
201
+ }
202
+ await rename(temporary, path);
203
+ return path;
204
+ }
205
+ finally {
206
+ await release();
207
+ }
208
+ }
209
+ function diagnosticCredentialEnvironment(argv) {
210
+ const env = { ...process.env };
211
+ for (const [flag, name] of [["--profile", "OXYGEN_PROFILE"], ["--org", "OXYGEN_ORG"]]) {
212
+ const at = argv.indexOf(flag);
213
+ const equals = argv.find(value => value.startsWith(flag + "="));
214
+ const value = equals?.slice(flag.length + 1) ?? (at >= 0 ? argv[at + 1] : undefined);
215
+ if (value && !value.startsWith("-"))
216
+ env[name] = value;
217
+ }
218
+ return env;
219
+ }
@@ -1,8 +1,11 @@
1
+ import { type GeoExportPagination } from "@oxygen/shared/geo";
1
2
  import { type StoredCredentials } from "./credentials.js";
2
3
  import { type CliEndpoint } from "./runtime.js";
3
4
  type RequestOptions = {
4
5
  /** Internal bounded file download; never follows a caller-supplied URL. */
5
6
  binaryMaxBytes?: number;
7
+ /** Return pagination beside bytes for operations advertising paginated downloads. */
8
+ binaryMetadata?: boolean;
6
9
  /** Stream a native authenticated download to an exclusively created file. */
7
10
  binaryOutputPath?: string;
8
11
  method?: "GET" | "POST" | "PATCH" | "DELETE";
@@ -25,6 +28,10 @@ type RequestOptions = {
25
28
  };
26
29
  export declare function requestOxygen<T>(// skipcq: JS-R1005
27
30
  path: string, options?: RequestOptions): Promise<T>;
31
+ export type PaginatedFileDownload = {
32
+ bytes: Uint8Array;
33
+ pagination?: GeoExportPagination;
34
+ };
28
35
  export declare function ensureFreshCliForApiUrl(apiUrl: string, options?: {
29
36
  fetch?: typeof fetch;
30
37
  endpoint?: CliEndpoint;
@@ -1,5 +1,7 @@
1
+ import { readGeoExportPagination } from "@oxygen/shared/geo";
1
2
  import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, parseSemver, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
2
3
  import { randomUUID } from "node:crypto";
4
+ import { cliDiagnosticHeaders, cliDiagnosticInvocation, recordCliDiagnosticRequest } from "./diagnostics.js";
3
5
  import { open, unlink } from "node:fs/promises";
4
6
  import { recordMutatingRequestAnswered, recoverFromCliFloorRejection } from "./auto-update.js";
5
7
  import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
@@ -49,8 +51,9 @@ path, options = {}) {
49
51
  }
50
52
  }
51
53
  options.signal?.throwIfAborted();
52
- const traceId = options.traceId ?? randomUUID();
54
+ const traceId = options.traceId ?? cliDiagnosticInvocation() ?? randomUUID();
53
55
  const headers = {
56
+ ...cliDiagnosticHeaders(),
54
57
  Accept: "application/json",
55
58
  "X-Oxygen-Trace-Id": traceId,
56
59
  // Advertise the CLI version so the server can enforce its minimum-CLI floor
@@ -104,15 +107,19 @@ path, options = {}) {
104
107
  else if (timeout.signal)
105
108
  requestInit.signal = timeout.signal;
106
109
  try {
110
+ recordCliDiagnosticRequest({ event: "request", trace_id: traceId, api_url: apiUrl, path, method,
111
+ input: options.body ?? (options.formData ? { withheld: "multipart_binary" } : null) });
107
112
  response = await fetchImpl(`${apiUrl}${path}`, requestInit);
108
113
  if (response.ok && options.binaryOutputPath !== undefined) {
109
114
  return await streamBinaryResponse(response, options.binaryOutputPath);
110
115
  }
111
116
  if (response.ok && options.binaryMaxBytes !== undefined) {
112
- return await readBoundedBinaryResponse(response, options.binaryMaxBytes);
117
+ const bytes = await readBoundedBinaryResponse(response, options.binaryMaxBytes);
118
+ return (options.binaryMetadata ? { bytes, pagination: readGeoExportPagination(response.headers) } : bytes);
113
119
  }
114
120
  }
115
121
  catch (error) {
122
+ recordCliDiagnosticRequest({ event: "transport_failure", trace_id: traceId, error });
116
123
  if (error instanceof OxygenError)
117
124
  throw error;
118
125
  if (timeout.timedOut() || (timeoutMs !== undefined && isAbortError(error))) {
@@ -163,6 +170,7 @@ path, options = {}) {
163
170
  });
164
171
  });
165
172
  const compatibility = readEnvelopeCompatibility(envelope);
173
+ recordCliDiagnosticRequest({ event: "response", trace_id: traceId, status: response.status, output: envelope });
166
174
  const failed = !response.ok || !envelope.ok;
167
175
  try {
168
176
  assertCliMeetsMinimumApiVersion(compatibility, options, endpoint);
@@ -452,6 +460,7 @@ async function readEnvelope(response) {
452
460
  parsed = text ? JSON.parse(text) : null;
453
461
  }
454
462
  catch {
463
+ recordCliDiagnosticRequest({ event: "invalid_response", status: response.status, output: text });
455
464
  throw new OxygenError("invalid_response", "Oxygen API returned a non-JSON response.", {
456
465
  details: {
457
466
  status: response.status,
@@ -462,6 +471,7 @@ async function readEnvelope(response) {
462
471
  }
463
472
  if (isCliResult(parsed))
464
473
  return parsed;
474
+ recordCliDiagnosticRequest({ event: "invalid_response", status: response.status, output: parsed });
465
475
  throw new OxygenError("invalid_response", "Oxygen API returned an invalid response envelope.", {
466
476
  details: { status: response.status },
467
477
  exitCode: 1,