@oxygen-agent/cli 1.1011.1091 → 1.1013.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.d.ts +3 -1
- package/dist/command-manifest.js +11 -0
- package/dist/customer-operation-commands.js +7 -3
- package/dist/diagnostic-upload-main.d.ts +1 -0
- package/dist/diagnostic-upload-main.js +2 -0
- package/dist/diagnostic-uploader.d.ts +3 -0
- package/dist/diagnostic-uploader.js +68 -0
- package/dist/diagnostics.d.ts +22 -0
- package/dist/diagnostics.js +219 -0
- package/dist/http-client.d.ts +7 -0
- package/dist/http-client.js +12 -2
- package/dist/index.js +72 -34
- package/dist/transcript.js +1 -1
- package/node_modules/@oxygen/formula/dist/expression.js +16 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +15 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +28 -4
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/geo.js +160 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +10 -4
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
- package/node_modules/@oxygen/shared/dist/diagnostic-context.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-context.js +17 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +12 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +60 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-receipt.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-receipt.js +22 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +39 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +233 -0
- package/node_modules/@oxygen/shared/dist/diagnostics.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/diagnostics.js +253 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
- package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
- package/node_modules/@oxygen/shared/dist/geo.d.ts +264 -0
- package/node_modules/@oxygen/shared/dist/geo.js +342 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
- package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
- package/node_modules/@oxygen/shared/dist/onboarding-first-leads.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/onboarding-first-leads.js +25 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +4 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +2 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
- package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
- package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +4 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +46 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +51 -1
- package/node_modules/@oxygen/shared/package.json +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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. */
|
package/dist/command-manifest.js
CHANGED
|
@@ -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
|
|
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,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
|
+
}
|
package/dist/http-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/http-client.js
CHANGED
|
@@ -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
|
-
|
|
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,
|