@oxygen-agent/cli 1.1011.405 → 1.1011.440

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 (70) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +8 -4
  3. package/dist/connectors-commands.js +3 -0
  4. package/dist/contract-flags.js +22 -9
  5. package/dist/customer-operation-commands.d.ts +5 -0
  6. package/dist/customer-operation-commands.js +101 -0
  7. package/dist/database-query-flags.d.ts +2 -0
  8. package/dist/database-query-flags.js +6 -0
  9. package/dist/help.d.ts +3 -1
  10. package/dist/help.js +33 -0
  11. package/dist/http-client.d.ts +2 -0
  12. package/dist/http-client.js +41 -1
  13. package/dist/index.js +470 -174
  14. package/dist/knowledge-repository-commands.js +12 -14
  15. package/dist/mailbox-ramp-notice.d.ts +11 -0
  16. package/dist/mailbox-ramp-notice.js +53 -0
  17. package/dist/settings-commands.js +5 -4
  18. package/dist/skills.d.ts +5 -4
  19. package/dist/skills.js +106 -79
  20. package/dist/tool-catalog-detail-command.d.ts +7 -0
  21. package/dist/tool-catalog-detail-command.js +19 -0
  22. package/dist/tool-catalog-search-window.d.ts +6 -0
  23. package/dist/tool-catalog-search-window.js +12 -0
  24. package/dist/transcript.js +1 -0
  25. package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +1 -1
  26. package/node_modules/@oxygen/shared/dist/bounded-read-result.js +2 -2
  27. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -1
  28. package/node_modules/@oxygen/shared/dist/capability-discovery.js +359 -34
  29. package/node_modules/@oxygen/shared/dist/connection-approval.d.ts +2 -0
  30. package/node_modules/@oxygen/shared/dist/connection-approval.js +2 -0
  31. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.d.ts +2 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +110 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +4 -0
  34. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +39 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.d.ts +21 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.js +15 -0
  37. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.d.ts +2 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +131 -0
  39. package/node_modules/@oxygen/shared/dist/contracts/database.d.ts +30 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/database.js +29 -0
  41. package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.d.ts +43 -0
  42. package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +20 -0
  43. package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.d.ts +12 -0
  44. package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +8 -0
  45. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +48 -2
  46. package/node_modules/@oxygen/shared/dist/contracts/operations.js +67 -7
  47. package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.d.ts +4 -0
  48. package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.js +6 -0
  49. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +21 -0
  50. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +27 -0
  51. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.d.ts +19 -0
  52. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.js +10 -0
  53. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.d.ts +12 -0
  54. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.js +7 -0
  55. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +1 -0
  56. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +12 -0
  57. package/node_modules/@oxygen/shared/dist/email-campaign-ramp.d.ts +104 -0
  58. package/node_modules/@oxygen/shared/dist/email-campaign-ramp.js +250 -0
  59. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +1 -2
  60. package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -2
  61. package/node_modules/@oxygen/shared/dist/provider-input-value.d.ts +5 -0
  62. package/node_modules/@oxygen/shared/dist/provider-input-value.js +10 -0
  63. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  64. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  65. package/node_modules/@oxygen/shared/package.json +3 -6
  66. package/package.json +1 -1
  67. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +0 -15
  68. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +0 -31
  69. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +0 -41
  70. package/node_modules/@oxygen/shared/dist/copilot-skills.js +0 -6
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.405
37
+ Version: 1.1011.440
@@ -1,3 +1,4 @@
1
+ import { OPERATION_SPECS } from "@oxygen/shared/contracts/operations";
1
2
  import { CLI_EXIT_CODE_TABLE, OXYGEN_VERSION, inferUserCapabilityRoute, serializeCapabilityRoute, } from "@oxygen/shared";
2
3
  const DEFAULT_COMMAND_SEARCH_LIMIT = 10;
3
4
  const MAX_COMMAND_SEARCH_LIMIT = 25;
@@ -341,6 +342,7 @@ function toManifestEntry(command, path) {
341
342
  const leafVerb = path[path.length - 1] ?? "";
342
343
  const verbPrefix = leafVerb.split("-")[0] ?? leafVerb;
343
344
  const commandName = path.join(" ");
345
+ const operations = OPERATION_SPECS.filter((spec) => spec.cli.path.join(" ") === commandName);
344
346
  return {
345
347
  name: commandName,
346
348
  group: path[0] ?? "",
@@ -355,7 +357,9 @@ function toManifestEntry(command, path) {
355
357
  description: option.description,
356
358
  required: option.mandatory,
357
359
  })),
358
- spends_credits: !ZERO_CREDIT_APPROVAL_COMMANDS.has(commandName) &&
360
+ // Some native commands read without flags and write with an explicit grant.
361
+ // Their metadata must include every available action's authorization posture.
362
+ spends_credits: operations.length ? operations.some((spec) => spec.spends) : (!ZERO_CREDIT_APPROVAL_COMMANDS.has(commandName) &&
359
363
  (flagStrings.some((flags) => flags.includes("--approved") ||
360
364
  flags.includes("--max-credits") ||
361
365
  // Legacy Copilot compatibility plus any future explicit budget flag.
@@ -368,11 +372,11 @@ function toManifestEntry(command, path) {
368
372
  // cap flag exists for either, so the flag heuristic alone would misreport
369
373
  // them as free.
370
374
  (path[0] === "copilot" &&
371
- (leafVerb === "send" || leafVerb === "approve"))),
372
- mutates: !READ_ONLY_COMMANDS.has(commandName)
375
+ (leafVerb === "send" || leafVerb === "approve")))),
376
+ mutates: operations.length ? operations.some((spec) => spec.mutates) : (!READ_ONLY_COMMANDS.has(commandName)
373
377
  && (MUTATING_COMMANDS.has(commandName)
374
378
  || MUTATING_VERBS.has(leafVerb)
375
- || MUTATING_VERBS.has(verbPrefix)),
379
+ || MUTATING_VERBS.has(verbPrefix))),
376
380
  preview_by_default: flagStrings.some((flags) => flags.includes("--live")) ||
377
381
  PREVIEW_BY_DEFAULT_COMMANDS.has(commandName),
378
382
  json_supported: flagStrings.some((flags) => flags.includes("--json")),
@@ -49,6 +49,9 @@ export function buildConnectorUpdateBody(options) {
49
49
  if (!("default_workspace" in body) && !("credit_ceiling" in body)) {
50
50
  throw new OxygenError("invalid_request", "Pass --default-workspace <workspace> (or --clear-default-workspace) to set where the app works, or --credit-ceiling <credits> (or --clear-credit-ceiling) to set what it may spend in 24 hours.", { exitCode: 1 });
51
51
  }
52
+ if ("approve_above_default" in body && !("credit_ceiling" in body)) {
53
+ throw new OxygenError("invalid_request", "--approve-above-default only goes with --credit-ceiling (or --clear-credit-ceiling).", { exitCode: 1 });
54
+ }
52
55
  return body;
53
56
  }
54
57
  export function registerConnectorsCommands(program, handle) {
@@ -24,7 +24,7 @@ function attribute(flag) {
24
24
  return flag.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase());
25
25
  }
26
26
  function isObject(property) {
27
- return "type" in property && property.type === "object" && property.properties !== undefined;
27
+ return "type" in property && property.type === "object";
28
28
  }
29
29
  function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = "") {
30
30
  const specs = [];
@@ -37,11 +37,14 @@ function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = ""
37
37
  const flag = entry.flag ?? `${parentFlag}${kebab(name)}`;
38
38
  if (isObject(property)) {
39
39
  const fields = Object.values(property.properties ?? {});
40
- const splat = entry.flatten || (fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
40
+ const splat = entry.flatten || (fields.length > 0 && property.additionalProperties !== true && fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
41
41
  if (splat)
42
42
  specs.push(...contractFlagSpecs(property.properties ?? {}, overlay, path, entry.flatten ? "" : `${flag}-`));
43
43
  else
44
- specs.push({ kind: "json", path, flag: `${flag}-json`, entry });
44
+ specs.push({ kind: "json", path, flag: `${flag}-json`, property, entry });
45
+ }
46
+ else if (!("type" in property) || (property.type === "array" && !("type" in property.items && ["string", "number", "integer", "boolean"].includes(property.items.type)))) {
47
+ specs.push({ kind: "json", path, flag: `${flag}-json`, property, entry });
45
48
  }
46
49
  else if ("type" in property && property.type === "boolean") {
47
50
  specs.push(entry.pair ? { kind: "pair", path, on: entry.pair.on, off: entry.pair.off, entry } : { kind: "boolean", path, flag, entry });
@@ -97,7 +100,7 @@ export function addContractFlags(command, contract, overlay = {}) {
97
100
  return command;
98
101
  }
99
102
  function invalid(flag, value, message, code = "invalid_number") {
100
- return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 1 });
103
+ return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 2 });
101
104
  }
102
105
  function parseScalar(property, raw, flag) {
103
106
  if (!("type" in property))
@@ -121,6 +124,13 @@ function parseScalar(property, raw, flag) {
121
124
  if (property.type === "string" && property.enum && !property.enum.includes(raw)) {
122
125
  throw invalid(flag, raw, `--${flag} must be one of: ${property.enum.join(", ")}.`, "invalid_option");
123
126
  }
127
+ if (property.type === "boolean") {
128
+ if (raw === "true")
129
+ return true;
130
+ if (raw === "false")
131
+ return false;
132
+ throw invalid(flag, raw, `--${flag} values must be true or false.`, "invalid_option");
133
+ }
124
134
  return raw;
125
135
  }
126
136
  function readValue(spec, raw) {
@@ -147,8 +157,8 @@ function setPath(body, path, value) {
147
157
  function stringOption(options, flag, aliases = []) {
148
158
  for (const name of [flag, ...aliases]) {
149
159
  const value = options[attribute(name)];
150
- if (typeof value === "string" && value.trim())
151
- return value.trim();
160
+ if (typeof value === "string")
161
+ return value;
152
162
  }
153
163
  return undefined;
154
164
  }
@@ -177,11 +187,14 @@ export function readContractFlags(options, contract, overlay = {}) {
177
187
  parsed = JSON.parse(raw);
178
188
  }
179
189
  catch {
180
- throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
190
+ throw invalid(spec.flag, raw, `--${spec.flag} must be valid JSON.`, "invalid_json");
181
191
  }
182
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
192
+ if ("type" in spec.property && spec.property.type === "object" && (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))) {
183
193
  throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
184
194
  }
195
+ if ("type" in spec.property && spec.property.type === "array" && !Array.isArray(parsed)) {
196
+ throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON array.`, "invalid_json");
197
+ }
185
198
  setPath(body, spec.path, parsed);
186
199
  given.add(spec.flag);
187
200
  }
@@ -198,7 +211,7 @@ export function readContractFlags(options, contract, overlay = {}) {
198
211
  }
199
212
  else if (options[attribute(spec.flag)] === true) {
200
213
  if (given.has(spec.target)) {
201
- throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 1 });
214
+ throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 2 });
202
215
  }
203
216
  setPath(body, spec.path, null);
204
217
  }
@@ -0,0 +1,5 @@
1
+ import { Command } from "commander";
2
+ /** Register each domain's selected operation as a native command with exact typed flags. */
3
+ export declare function registerCustomerOperationCommands(program: Command, handle: (command: string, options: {
4
+ json?: boolean;
5
+ }, action: () => Promise<unknown>) => Promise<void>): void;
@@ -0,0 +1,101 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { basename, extname, resolve } from "node:path";
3
+ import { OxygenError } from "@oxygen/shared/cli-result";
4
+ import { CLI_OPERATIONS, operationFormData, operationInputContract, operationRequest } from "@oxygen/shared/contracts/operations";
5
+ import { unwrapNullable, validateContract } from "@oxygen/shared/contracts/schema";
6
+ import { addContractFlags, readContractFlags } from "./contract-flags.js";
7
+ import { requestOxygen } from "./http-client.js";
8
+ /** Register each domain's selected operation as a native command with exact typed flags. */
9
+ export function registerCustomerOperationCommands(program, handle) {
10
+ for (const spec of CLI_OPERATIONS) {
11
+ let parent = program;
12
+ const words = [...spec.cli.path];
13
+ const leaf = words.pop();
14
+ for (const word of words) {
15
+ let child = parent.commands.find((command) => command.name() === word);
16
+ if (!child)
17
+ child = parent.command(word).description(`${word} workspace operations.`);
18
+ parent = child;
19
+ }
20
+ // Never hide an existing owner behind a second Commander command.
21
+ if (parent.commands.some((command) => command.name() === leaf)) {
22
+ throw new Error(`Duplicate native operation command ${spec.cli.path.join(" ")}`);
23
+ }
24
+ const contract = operationInputContract(spec);
25
+ const command = addContractFlags(parent.command(leaf).description(spec.cli.summary), contract)
26
+ .option("--json", "Print a JSON envelope.");
27
+ if (spec.binary)
28
+ command.requiredOption("--output <path>", "Write the exact downloaded bytes to this local file.");
29
+ if (spec.multipart)
30
+ command.option("--file <path>", "Upload bytes from this local file. Omit the base64 flag; file name and content type default from the path.");
31
+ for (const name of contract.required ?? []) {
32
+ const property = contract.properties[name];
33
+ const flag = `--${name.replace(/_/g, "-")}`;
34
+ const option = command.options.find((candidate) => candidate.long === flag || candidate.long === `${flag}-json`);
35
+ const multipartAlternative = spec.multipart && [spec.multipart.contentField, spec.multipart.nameField, spec.multipart.typeField].includes(name);
36
+ if (multipartAlternative) {
37
+ if (option)
38
+ option.description = `${option.description} Required unless supplied by --file.`.trim();
39
+ }
40
+ else if (unwrapNullable(property).nullable) {
41
+ if (option)
42
+ option.description = `${option.description} Required: use this flag or --clear-${name.replace(/_/g, "-")}.`.trim();
43
+ }
44
+ else if (option) {
45
+ // The negative boolean spelling uses the same attribute, so an explicit
46
+ // false satisfies this requirement just as true does.
47
+ option.makeOptionMandatory();
48
+ }
49
+ }
50
+ command.action((options, selected) => handle(spec.cli.path.join(" "), { ...options, json: selected.optsWithGlobals().json === true }, async () => {
51
+ const input = readContractFlags(options, contract);
52
+ if (spec.multipart && options.file !== undefined) {
53
+ if (input[spec.multipart.contentField] !== undefined)
54
+ throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
55
+ const filePath = String(options.file);
56
+ input[spec.multipart.contentField] = (await readFile(filePath)).toString("base64");
57
+ input[spec.multipart.nameField] ??= basename(filePath);
58
+ input[spec.multipart.typeField] ??= uploadContentType(filePath);
59
+ }
60
+ const validation = validateContract(contract, input, { unknown: "reject", overMax: "reject" });
61
+ if (!validation.ok)
62
+ throw new OxygenError("invalid_request", validation.issues.map((issue) => issue.message).join(" "), { exitCode: 2 });
63
+ const request = operationRequest(spec, input);
64
+ if (spec.binary) {
65
+ if (spec.binary.stream)
66
+ return requestOxygen(request.path, { method: request.method, binaryOutputPath: resolve(String(options.output)) });
67
+ const bytes = await requestOxygen(request.path, { method: request.method, binaryMaxBytes: spec.binary.maxBytes });
68
+ const output = resolve(String(options.output));
69
+ try {
70
+ await writeFile(output, bytes, { flag: "wx" });
71
+ }
72
+ catch (error) {
73
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
74
+ throw new OxygenError("output_exists", "The output file already exists. Choose another --output path.", { exitCode: 2 });
75
+ }
76
+ throw error;
77
+ }
78
+ return { path: output, bytes: bytes.byteLength };
79
+ }
80
+ if (spec.multipart) {
81
+ let formData;
82
+ try {
83
+ formData = operationFormData(spec, request.body ?? {});
84
+ }
85
+ catch (error) {
86
+ throw new OxygenError("invalid_request", error instanceof Error ? error.message : "Invalid upload.", { exitCode: 2 });
87
+ }
88
+ return requestOxygen(request.path, { method: request.method, formData });
89
+ }
90
+ return requestOxygen(request.path, { method: request.method, ...(request.body ? { body: request.body } : {}) });
91
+ }));
92
+ }
93
+ }
94
+ function uploadContentType(filePath) {
95
+ const types = {
96
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml",
97
+ ".csv": "text/csv", ".json": "application/json", ".jsonl": "application/x-ndjson",
98
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
99
+ };
100
+ return types[extname(filePath).toLowerCase()] ?? "application/octet-stream";
101
+ }
@@ -0,0 +1,2 @@
1
+ import type { ContractFlagOverlay } from "./contract-flags.js";
2
+ export declare const DATABASE_QUERY_FLAG_OVERLAY: ContractFlagOverlay;
@@ -0,0 +1,6 @@
1
+ import { readPositiveInt } from "./cli-values.js";
2
+ export const DATABASE_QUERY_FLAG_OVERLAY = {
3
+ sql: { omit: true }, // Keep the existing required --sql option.
4
+ max_rows: { valueName: "<n>", parse: readPositiveInt }, // Preserve legacy coercion/defaults.
5
+ max_chars: { valueName: "<n>" },
6
+ };
package/dist/help.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Command, CommanderError } from "commander";
1
+ import { CommanderError, type Command } from "commander";
2
2
  import { type CliFailure } from "@oxygen/shared/cli-result";
3
3
  export declare function applyOxygenHelp(program: Command, binaryName: string): void;
4
4
  /**
@@ -12,6 +12,8 @@ export declare function applyOxygenHelp(program: Command, binaryName: string): v
12
12
  * `installCommanderExitOverride` recurses.
13
13
  */
14
14
  export declare function enableCommandSuggestions(command: Command): void;
15
+ /** A declared flag following a value option is a missing value, not literal data. */
16
+ export declare function validateRequiredOptionValues(program: Command, selected: Command): void;
15
17
  /**
16
18
  * The extra line printed after `error: unknown command '<x>'`.
17
19
  *
package/dist/help.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { CommanderError } from "commander";
1
2
  import { CLI_EXIT_CODE_TABLE } from "@oxygen/shared";
2
3
  import { exitCodeForErrorCode, failure } from "@oxygen/shared/cli-result";
3
4
  // Grouped `--help` for the ~70 top-level command groups: commander's default
@@ -181,6 +182,38 @@ export function enableCommandSuggestions(command) {
181
182
  for (const child of command.commands)
182
183
  enableCommandSuggestions(child);
183
184
  }
185
+ /** A declared flag following a value option is a missing value, not literal data. */
186
+ export function validateRequiredOptionValues(program, selected) {
187
+ const options = new Map();
188
+ for (let current = selected; current; current = current.parent) {
189
+ for (const option of current.options) {
190
+ if (option.long && !options.has(option.long))
191
+ options.set(option.long, option);
192
+ if (option.short && !options.has(option.short))
193
+ options.set(option.short, option);
194
+ }
195
+ }
196
+ // Installed Commander writes this exact property before command dispatch;
197
+ // its public typings omit it. Validate only that property at this seam.
198
+ const rawArgs = Reflect.get(program, "rawArgs");
199
+ if (!Array.isArray(rawArgs) || !rawArgs.every((value) => typeof value === "string")) {
200
+ throw new CommanderError(2, "commander.invalidArgument", "Cannot validate the command's original option arguments.");
201
+ }
202
+ const argv = rawArgs;
203
+ for (let index = 0; index < argv.length; index += 1) {
204
+ const token = argv[index];
205
+ if (token === "--")
206
+ break;
207
+ const option = options.get(token);
208
+ if (!option?.required)
209
+ continue;
210
+ const next = argv[index + 1];
211
+ if (next === undefined || next === "--" || options.has(next.split("=")[0])) {
212
+ throw new CommanderError(2, "commander.optionMissingArgument", `option '${option.flags}' argument missing. To use an option name as literal text, pass ${option.long ?? option.short}=<value>.`);
213
+ }
214
+ index += 1;
215
+ }
216
+ }
184
217
  /**
185
218
  * Verbs a user reaches for that this CLI spells differently.
186
219
  *
@@ -3,6 +3,8 @@ import { type CliEndpoint } from "./runtime.js";
3
3
  type RequestOptions = {
4
4
  /** Internal bounded file download; never follows a caller-supplied URL. */
5
5
  binaryMaxBytes?: number;
6
+ /** Stream a native authenticated download to an exclusively created file. */
7
+ binaryOutputPath?: string;
6
8
  method?: "GET" | "POST" | "PATCH" | "DELETE";
7
9
  body?: Record<string, unknown>;
8
10
  formData?: FormData;
@@ -1,5 +1,6 @@
1
1
  import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, parseSemver, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { open, unlink } from "node:fs/promises";
3
4
  import { recordMutatingRequestAnswered, recoverFromCliFloorRejection } from "./auto-update.js";
4
5
  import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
5
6
  import { assertResolvedApiUrl, cliEndpointDetails, describeCliEndpoint, isProdApiUrl, resolveCliUpdateGuidance, } from "./runtime.js";
@@ -86,7 +87,7 @@ path, options = {}) {
86
87
  const requestInit = {
87
88
  method: options.method ?? "GET",
88
89
  headers,
89
- ...(options.binaryMaxBytes === undefined ? {} : { redirect: "error" }),
90
+ ...(options.binaryMaxBytes === undefined && options.binaryOutputPath === undefined ? {} : { redirect: "error" }),
90
91
  ...(options.body ? { body: JSON.stringify(options.body) } : {}),
91
92
  ...(options.formData ? { body: options.formData } : {}),
92
93
  };
@@ -97,6 +98,9 @@ path, options = {}) {
97
98
  requestInit.signal = timeout.signal;
98
99
  try {
99
100
  response = await fetchImpl(`${apiUrl}${path}`, requestInit);
101
+ if (response.ok && options.binaryOutputPath !== undefined) {
102
+ return await streamBinaryResponse(response, options.binaryOutputPath);
103
+ }
100
104
  if (response.ok && options.binaryMaxBytes !== undefined) {
101
105
  return await readBoundedBinaryResponse(response, options.binaryMaxBytes);
102
106
  }
@@ -195,6 +199,42 @@ async function readBoundedBinaryResponse(response, maxBytes) {
195
199
  }
196
200
  return bytes;
197
201
  }
202
+ async function streamBinaryResponse(response, output) {
203
+ let file;
204
+ try {
205
+ file = await open(output, "wx");
206
+ }
207
+ catch (error) {
208
+ await response.body?.cancel();
209
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
210
+ throw new OxygenError("output_exists", "The output file already exists. Choose another --output path.", { exitCode: 2 });
211
+ }
212
+ throw error;
213
+ }
214
+ const reader = response.body?.getReader();
215
+ let bytes = 0;
216
+ try {
217
+ if (!reader)
218
+ throw new OxygenError("invalid_response", "The download had no body.");
219
+ for (;;) {
220
+ const chunk = await reader.read();
221
+ if (chunk.done)
222
+ break;
223
+ await file.writeFile(chunk.value);
224
+ bytes += chunk.value.byteLength;
225
+ }
226
+ await file.close();
227
+ return { path: output, bytes };
228
+ }
229
+ catch (error) {
230
+ await file.close().catch(() => { });
231
+ await unlink(output).catch(() => { });
232
+ throw error;
233
+ }
234
+ finally {
235
+ await reader?.cancel().catch(() => { });
236
+ }
237
+ }
198
238
  export async function ensureFreshCliForApiUrl(apiUrl, options = {}) {
199
239
  const checkKey = `${apiUrl}|${OXYGEN_VERSION}`;
200
240
  if (cliCompatibilityCheckKeys.has(checkKey))