poe-code 3.0.260 → 3.0.261

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.
@@ -1 +1 @@
1
- export { GASLIGHT_CONFIG_EXAMPLE, ingestGaslight, loadGaslightConfig, parseGaslightConfig, runGaslight, type GaslightConfig, type GaslightCollectHumanPrompts, type GaslightEvent, type GaslightFileSystem, type GaslightIngestEvent, type GaslightIngestOptions, type GaslightIngestResult, type GaslightOptions, type GaslightResult, type GaslightRound, type GaslightSpawn } from "../../packages/agent-gaslight/dist/index.js";
1
+ export { GASLIGHT_CONFIG_EXAMPLE, ingestGaslight, loadGaslightConfig, parseGaslightConfig, runGaslight, type GaslightConfig, type GaslightCollectHumanPrompts, type GaslightEvent, type GaslightFileSystem, type GaslightIngestEvent, type GaslightIngestOptions, type GaslightIngestResult, type GaslightOptions, type GaslightPlanResult, type GaslightResult, type GaslightRound, type GaslightSpawn } from "../../packages/agent-gaslight/dist/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"gaslight.js","sourceRoot":"","sources":["../../src/sdk/gaslight.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EAYZ,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"gaslight.js","sourceRoot":"","sources":["../../src/sdk/gaslight.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EAaZ,MAAM,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.260",
3
+ "version": "3.0.261",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,4 +1,4 @@
1
1
  export { GASLIGHT_CONFIG_EXAMPLE, loadGaslightConfig, parseGaslightConfig } from "./config.js";
2
2
  export { ingestGaslight } from "./ingest.js";
3
3
  export { runGaslight } from "./run.js";
4
- export type { GaslightConfig, GaslightCollectHumanPrompts, GaslightEvent, GaslightFileSystem, GaslightIngestEvent, GaslightIngestOptions, GaslightIngestResult, GaslightOptions, GaslightResult, GaslightRound, GaslightSpawn } from "./types.js";
4
+ export type { GaslightConfig, GaslightCollectHumanPrompts, GaslightEvent, GaslightFileSystem, GaslightIngestEvent, GaslightIngestOptions, GaslightIngestResult, GaslightOptions, GaslightPlanResult, GaslightResult, GaslightRound, GaslightSpawn } from "./types.js";
@@ -45,46 +45,92 @@ function validateInlineConfig(prompt, followups) {
45
45
  throw new Error("followups must be a non-empty array of non-empty strings.");
46
46
  }
47
47
  }
48
+ function resolvePlanPaths(options) {
49
+ if (options.planPaths.length === 0) {
50
+ throw new Error("Provide at least one plan path.");
51
+ }
52
+ for (const planPath of options.planPaths) {
53
+ if (planPath.trim().length === 0) {
54
+ throw new Error("plan paths must be non-empty strings.");
55
+ }
56
+ }
57
+ return options.planPaths.map((planPath) => planPath.trim());
58
+ }
48
59
  export async function runGaslight(options) {
49
60
  const cwd = options.cwd ?? process.cwd();
50
61
  const homeDir = options.homeDir ?? os.homedir();
51
62
  const fs = options.fs ?? nodeFs;
52
63
  const spawn = options.spawn ?? defaultSpawn;
53
64
  validateInlineConfig(options.prompt, options.followups);
54
- await requirePlan(fs, cwd, options.planPath);
65
+ const planPaths = resolvePlanPaths(options);
66
+ for (const planPath of planPaths) {
67
+ await requirePlan(fs, cwd, planPath);
68
+ }
55
69
  const config = options.prompt !== undefined && options.followups !== undefined
56
70
  ? { prompt: options.prompt.trim(), followups: options.followups.map((value) => value.trim()) }
57
71
  : await loadGaslightConfig(cwd, homeDir, fs, options.configPath);
58
- const prompts = [`${config.prompt} ${options.planPath}`, ...config.followups];
59
72
  const rounds = [];
73
+ const plans = [];
60
74
  let usage;
61
- let resumeThreadId;
62
- for (const [index, prompt] of prompts.entries()) {
63
- const round = index + 1;
64
- options.onEvent?.({ type: "round.started", round, total: prompts.length, prompt });
65
- const result = await spawn(options.agent, {
66
- prompt,
67
- cwd,
68
- mode: options.mode ?? "edit",
69
- ...(options.model ? { model: options.model } : {}),
70
- ...(resumeThreadId ? { resumeThreadId } : {}),
71
- ...(options.signal ? { signal: options.signal } : {})
72
- });
73
- if (result.exitCode !== 0) {
74
- const completed = rounds.length;
75
- const noun = completed === 1 ? "round" : "rounds";
76
- throw new Error(`Gaslight round ${round} failed after ${completed} completed ${noun}: ${summarize(result.stderr, result.stdout) || `exit code ${result.exitCode}`}`);
77
- }
78
- const summary = summarize(result.stdout, result.stderr);
79
- rounds.push({ prompt, summary, ...(result.threadId ? { threadId: result.threadId } : {}) });
80
- usage = addUsage(usage, result.usage);
81
- options.onEvent?.({ type: "round.finished", round, total: prompts.length, summary });
82
- if (round < prompts.length) {
83
- if (!result.threadId) {
84
- throw new Error("agent returned no threadId; cannot resume the conversation");
75
+ for (const [planIndex, planPath] of planPaths.entries()) {
76
+ const prompts = [`${config.prompt} ${planPath}`, ...config.followups];
77
+ const planRounds = [];
78
+ let planUsage;
79
+ let resumeThreadId;
80
+ for (const [index, prompt] of prompts.entries()) {
81
+ const round = index + 1;
82
+ options.onEvent?.({
83
+ type: "round.started",
84
+ round,
85
+ total: prompts.length,
86
+ prompt,
87
+ planPath,
88
+ planIndex: planIndex + 1,
89
+ totalPlans: planPaths.length
90
+ });
91
+ const result = await spawn(options.agent, {
92
+ prompt,
93
+ cwd,
94
+ mode: options.mode ?? "edit",
95
+ ...(options.model ? { model: options.model } : {}),
96
+ ...(resumeThreadId ? { resumeThreadId } : {}),
97
+ ...(options.signal ? { signal: options.signal } : {})
98
+ });
99
+ if (result.exitCode !== 0) {
100
+ const completed = planRounds.length;
101
+ const noun = completed === 1 ? "round" : "rounds";
102
+ const prefix = planPaths.length === 1
103
+ ? `Gaslight round ${round}`
104
+ : `Gaslight plan ${planIndex + 1}/${planPaths.length} (${planPath}) round ${round}`;
105
+ throw new Error(`${prefix} failed after ${completed} completed ${noun}: ${summarize(result.stderr, result.stdout) || `exit code ${result.exitCode}`}`);
106
+ }
107
+ const summary = summarize(result.stdout, result.stderr);
108
+ const gaslightRound = {
109
+ prompt,
110
+ summary,
111
+ ...(result.threadId ? { threadId: result.threadId } : {})
112
+ };
113
+ planRounds.push(gaslightRound);
114
+ rounds.push(gaslightRound);
115
+ planUsage = addUsage(planUsage, result.usage);
116
+ usage = addUsage(usage, result.usage);
117
+ options.onEvent?.({
118
+ type: "round.finished",
119
+ round,
120
+ total: prompts.length,
121
+ summary,
122
+ planPath,
123
+ planIndex: planIndex + 1,
124
+ totalPlans: planPaths.length
125
+ });
126
+ if (round < prompts.length) {
127
+ if (!result.threadId) {
128
+ throw new Error("agent returned no threadId; cannot resume the conversation");
129
+ }
130
+ resumeThreadId = result.threadId;
85
131
  }
86
- resumeThreadId = result.threadId;
87
132
  }
133
+ plans.push({ planPath, rounds: planRounds, ...(planUsage ? { usage: planUsage } : {}) });
88
134
  }
89
- return { rounds, ...(usage ? { usage } : {}) };
135
+ return { rounds, plans, ...(usage ? { usage } : {}) };
90
136
  }
@@ -7,11 +7,17 @@ export type GaslightEvent = {
7
7
  round: number;
8
8
  total: number;
9
9
  prompt: string;
10
+ planPath: string;
11
+ planIndex: number;
12
+ totalPlans: number;
10
13
  } | {
11
14
  type: "round.finished";
12
15
  round: number;
13
16
  total: number;
14
17
  summary: string;
18
+ planPath: string;
19
+ planIndex: number;
20
+ totalPlans: number;
15
21
  };
16
22
  export interface GaslightRound {
17
23
  prompt: string;
@@ -20,10 +26,16 @@ export interface GaslightRound {
20
26
  }
21
27
  export interface GaslightResult {
22
28
  rounds: GaslightRound[];
29
+ plans: GaslightPlanResult[];
23
30
  usage?: SpawnUsage;
24
31
  }
25
- export interface GaslightOptions {
32
+ export interface GaslightPlanResult {
26
33
  planPath: string;
34
+ rounds: GaslightRound[];
35
+ usage?: SpawnUsage;
36
+ }
37
+ export interface GaslightOptions {
38
+ planPaths: string[];
27
39
  agent: string;
28
40
  model?: string;
29
41
  mode?: Exclude<SpawnMode, "auto">;
@@ -43,6 +43,15 @@ const TRANSPORT_PARAMS = [
43
43
  definition: { kind: "boolean" }
44
44
  }
45
45
  ];
46
+ const BINARY_OUTPUT_PARAM = {
47
+ paramName: "output",
48
+ sourceName: "output",
49
+ location: "transport",
50
+ description: "Write a binary response body to this local file path.",
51
+ scope: ["cli"],
52
+ optional: true,
53
+ definition: { kind: "string" }
54
+ };
46
55
  const SCHEMA_OPTION_SOURCES = [
47
56
  {
48
57
  key: "description",
@@ -176,7 +185,7 @@ function createGeneratedCommand(document, entry) {
176
185
  const response = resolveSuccessResponse(document, operation, operationId);
177
186
  const noun = createSafeGeneratedNoun(deriveNoun(operation, entry.path, operationId));
178
187
  const verb = deriveVerb(entry.method, entry.path, operation, operationId, noun);
179
- const collected = collectParams(document, entry, operation, operationId, auth);
188
+ const collected = collectParams(document, entry, operation, operationId, auth, response.mode);
180
189
  const methodDefaults = METHOD_DEFAULTS[entry.method];
181
190
  const exportName = `${toCamelCase(noun)}${toPascalCase(verb)}Command`;
182
191
  const filePath = `${noun}/${verb}.ts`;
@@ -220,11 +229,12 @@ function resolveOperationBaseUrl(operation, operationId) {
220
229
  throw new UserError(`Operation ${JSON.stringify(operationId)} defines invalid per-operation server URL ${JSON.stringify(server.url)}.`);
221
230
  }
222
231
  }
223
- function collectParams(document, entry, operation, operationId, auth) {
232
+ function collectParams(document, entry, operation, operationId, auth, responseMode) {
233
+ const transportParams = responseMode === "binary" ? [...TRANSPORT_PARAMS, BINARY_OUTPUT_PARAM] : TRANSPORT_PARAMS;
224
234
  const operationParams = collectOperationParameters(document, entry.path, entry.pathItem.parameters ?? [], operation.parameters ?? [], operationId, auth);
225
235
  const requestBodyParams = collectRequestBodyParams(document, operation, operationId, entry.method);
226
- const qualifiedRequestBodyParams = qualifyBodyParamCollisions(requestBodyParams, new Set([...operationParams.params, ...TRANSPORT_PARAMS].map((param) => param.paramName)));
227
- const params = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...TRANSPORT_PARAMS];
236
+ const qualifiedRequestBodyParams = qualifyBodyParamCollisions(requestBodyParams, new Set([...operationParams.params, ...transportParams].map((param) => param.paramName)));
237
+ const params = [...operationParams.params, ...qualifiedRequestBodyParams.params, ...transportParams];
228
238
  const deduped = new Map();
229
239
  for (const param of params) {
230
240
  const existing = deduped.get(param.paramName);
@@ -1284,13 +1294,24 @@ function applyDisambiguatedVerbs(candidates, existingPaths) {
1284
1294
  }
1285
1295
  function createCommandFile(options) {
1286
1296
  const requiresUserError = options.preflightBlocks.length > 0;
1297
+ const usesMultipartFileInputs = options.bodyMode === "multipart" &&
1298
+ options.multipartBinaryFields !== undefined &&
1299
+ options.multipartBinaryFields.length > 0;
1300
+ const usesBinaryOutput = options.responseMode === "binary";
1301
+ const usesRequestShapeVariable = usesMultipartFileInputs || usesBinaryOutput;
1302
+ const openApiImports = [
1303
+ "requestJson",
1304
+ "defineApiCommand",
1305
+ ...(usesMultipartFileInputs ? ["prepareMultipartFileInputs"] : []),
1306
+ ...(usesBinaryOutput ? ["writeBinaryResponseOutput"] : [])
1307
+ ];
1287
1308
  const lines = createGeneratedTypeScriptFileLines([
1288
1309
  `spec-sha: ${options.specSha}`,
1289
1310
  `operation-id: ${options.operationId}`
1290
1311
  ]);
1291
1312
  lines.push(requiresUserError
1292
1313
  ? 'import { S, UserError } from "toolcraft";'
1293
- : 'import { S } from "toolcraft";', 'import { requestJson, defineApiCommand } from "toolcraft-openapi";', "", `export const ${options.exportName} = defineApiCommand({`, ` name: ${JSON.stringify(options.verb)},`);
1314
+ : 'import { S } from "toolcraft";', `import { ${openApiImports.join(", ")} } from "toolcraft-openapi";`, "", `export const ${options.exportName} = defineApiCommand({`, ` name: ${JSON.stringify(options.verb)},`);
1294
1315
  if (options.description !== undefined) {
1295
1316
  lines.push(` description: ${JSON.stringify(options.description)},`);
1296
1317
  }
@@ -1303,9 +1324,30 @@ function createCommandFile(options) {
1303
1324
  lines.push(options.paramsSchemaOptions?.additionalProperties === false
1304
1325
  ? " }, { additionalProperties: false }),"
1305
1326
  : " }),");
1306
- lines.push(" handler: async ({ params, baseUrl, tokenSource, fetch }) => {");
1327
+ lines.push(usesRequestShapeVariable
1328
+ ? " handler: async ({ params, baseUrl, tokenSource, fetch, fs, env }) => {"
1329
+ : " handler: async ({ params, baseUrl, tokenSource, fetch }) => {");
1307
1330
  lines.push(...options.preflightBlocks.flatMap((block) => renderPreflightBlock(block)));
1308
- lines.push(" return requestJson({");
1331
+ if (usesRequestShapeVariable) {
1332
+ lines.push(" const requestShape = {");
1333
+ lines.push(...renderRequestShape(options.requestFields, options.sectionRenders, options.optionalSections));
1334
+ lines.push(" };");
1335
+ if (usesMultipartFileInputs) {
1336
+ lines.push(" const preparedRequestShape = await prepareMultipartFileInputs(requestShape, {");
1337
+ lines.push(' bodyMode: "multipart",');
1338
+ lines.push(` multipartBinaryFields: ${JSON.stringify(options.multipartBinaryFields)},`);
1339
+ lines.push(" fs,");
1340
+ lines.push(" env,");
1341
+ lines.push(" });");
1342
+ }
1343
+ else {
1344
+ lines.push(" const preparedRequestShape = requestShape;");
1345
+ }
1346
+ lines.push(" const result = await requestJson({");
1347
+ }
1348
+ else {
1349
+ lines.push(" return requestJson({");
1350
+ }
1309
1351
  lines.push(options.baseUrl === undefined
1310
1352
  ? " baseUrl,"
1311
1353
  : ` baseUrl: ${JSON.stringify(options.baseUrl)},`);
@@ -1340,8 +1382,16 @@ function createCommandFile(options) {
1340
1382
  lines.push(" fetch,");
1341
1383
  lines.push(" dryRun: params.dryRun,");
1342
1384
  lines.push(" verbose: params.verbose,");
1343
- lines.push(...renderRequestShape(options.requestFields, options.sectionRenders, options.optionalSections));
1385
+ if (usesRequestShapeVariable) {
1386
+ lines.push(" ...preparedRequestShape,");
1387
+ }
1388
+ else {
1389
+ lines.push(...renderRequestShape(options.requestFields, options.sectionRenders, options.optionalSections));
1390
+ }
1344
1391
  lines.push(" });");
1392
+ if (usesBinaryOutput) {
1393
+ lines.push(" return writeBinaryResponseOutput(result, params.output, { fs, env });");
1394
+ }
1345
1395
  lines.push(" },");
1346
1396
  lines.push("});");
1347
1397
  lines.push("");
@@ -1,3 +1,4 @@
1
+ import { type HandlerEnv, type HandlerFs } from "../../toolcraft/dist/index.js";
1
2
  import type { TokenSource } from "./auth/types.js";
2
3
  type QueryScalar = string | number | boolean | null | undefined;
3
4
  type QueryObject = {
@@ -45,6 +46,9 @@ export interface BinaryHttpResponse {
45
46
  byteLength: number;
46
47
  data: string;
47
48
  }
49
+ type RuntimeFileSystem = Pick<HandlerFs, "exists" | "readFile" | "writeFile">;
50
+ type RuntimeEnvironment = Pick<HandlerEnv, "get">;
51
+ type RequestShape = Partial<Pick<HttpRequestOptions, "pathParams" | "query" | "headers" | "body">>;
48
52
  export declare class HttpError extends Error {
49
53
  readonly status: number;
50
54
  readonly statusText: string;
@@ -58,4 +62,14 @@ export declare class HttpError extends Error {
58
62
  });
59
63
  }
60
64
  export declare function requestJson<TResult = unknown>(options: HttpRequestOptions): Promise<TResult | undefined>;
65
+ export declare function prepareMultipartFileInputs(requestShape: RequestShape, options: {
66
+ bodyMode?: HttpRequestOptions["bodyMode"];
67
+ multipartBinaryFields?: readonly string[];
68
+ fs?: RuntimeFileSystem;
69
+ env?: RuntimeEnvironment;
70
+ }): Promise<RequestShape>;
71
+ export declare function writeBinaryResponseOutput(result: unknown, outputPath: unknown, runtime: {
72
+ fs?: RuntimeFileSystem;
73
+ env?: RuntimeEnvironment;
74
+ }): Promise<unknown>;
61
75
  export {};
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { text as designText } from "toolcraft-design";
2
3
  import { UserError } from "toolcraft";
3
4
  import { classifyNetworkError } from "./network-error.js";
@@ -152,6 +153,58 @@ export async function requestJson(options) {
152
153
  }
153
154
  });
154
155
  }
156
+ export async function prepareMultipartFileInputs(requestShape, options) {
157
+ if (options.bodyMode !== "multipart" ||
158
+ options.multipartBinaryFields === undefined ||
159
+ options.multipartBinaryFields.length === 0 ||
160
+ options.fs === undefined ||
161
+ options.env === undefined ||
162
+ requestShape.body === undefined) {
163
+ return requestShape;
164
+ }
165
+ if (requestShape.body === null ||
166
+ typeof requestShape.body !== "object" ||
167
+ Array.isArray(requestShape.body)) {
168
+ return requestShape;
169
+ }
170
+ const body = { ...requestShape.body };
171
+ for (const field of options.multipartBinaryFields) {
172
+ const value = body[field];
173
+ if (typeof value !== "string") {
174
+ continue;
175
+ }
176
+ const filePath = resolveUserPath(value, options.env);
177
+ if (!(await options.fs.exists(filePath))) {
178
+ continue;
179
+ }
180
+ body[field] = {
181
+ data: await options.fs.readFile(filePath, "base64"),
182
+ filename: path.basename(filePath) || field
183
+ };
184
+ }
185
+ return {
186
+ ...requestShape,
187
+ body
188
+ };
189
+ }
190
+ export async function writeBinaryResponseOutput(result, outputPath, runtime) {
191
+ if (outputPath === undefined) {
192
+ return result;
193
+ }
194
+ if (runtime.fs === undefined || runtime.env === undefined) {
195
+ throw new UserError("Cannot write binary output without a Toolcraft file runtime.");
196
+ }
197
+ if (!isBinaryHttpResponse(result)) {
198
+ throw new UserError("Cannot write an empty binary response to an output path.");
199
+ }
200
+ const resolvedOutputPath = resolveUserPath(String(outputPath), runtime.env);
201
+ await runtime.fs.writeFile(resolvedOutputPath, result.data, { encoding: "base64" });
202
+ return {
203
+ output: resolvedOutputPath,
204
+ byteLength: result.byteLength,
205
+ contentType: result.contentType
206
+ };
207
+ }
155
208
  function buildRequestUrl(options) {
156
209
  const resolvedPath = substitutePathParams(options.path, options.pathParams);
157
210
  const baseUrl = new URL(options.baseUrl);
@@ -223,6 +276,22 @@ function decodeBase64Body(body) {
223
276
  }
224
277
  return Uint8Array.from(Buffer.from(body, "base64")).buffer;
225
278
  }
279
+ function decodeMultipartBinaryValue(value, fallbackFilename) {
280
+ if (typeof value === "string") {
281
+ return {
282
+ data: value,
283
+ filename: fallbackFilename
284
+ };
285
+ }
286
+ if (value !== null &&
287
+ typeof value === "object" &&
288
+ !Array.isArray(value) &&
289
+ typeof value.data === "string" &&
290
+ typeof value.filename === "string") {
291
+ return value;
292
+ }
293
+ throw new UserError("Multipart binary request fields must be base64 strings or resolved file inputs.");
294
+ }
226
295
  function serializeMultipartBody(body, binaryFields = []) {
227
296
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
228
297
  throw new UserError("Multipart request bodies must be objects.");
@@ -233,7 +302,8 @@ function serializeMultipartBody(body, binaryFields = []) {
233
302
  if (value === undefined)
234
303
  continue;
235
304
  if (binary.has(key)) {
236
- form.append(key, new Blob([decodeBase64Body(value)]), key);
305
+ const file = decodeMultipartBinaryValue(value, key);
306
+ form.append(key, new Blob([decodeBase64Body(file.data)]), file.filename);
237
307
  }
238
308
  else if (Array.isArray(value)) {
239
309
  for (const item of value)
@@ -245,6 +315,20 @@ function serializeMultipartBody(body, binaryFields = []) {
245
315
  }
246
316
  return form;
247
317
  }
318
+ function resolveUserPath(userPath, env) {
319
+ if (path.isAbsolute(userPath)) {
320
+ return path.normalize(userPath);
321
+ }
322
+ return path.resolve(env.get("INIT_CWD") ?? process.cwd(), userPath);
323
+ }
324
+ function isBinaryHttpResponse(value) {
325
+ return (value !== null &&
326
+ typeof value === "object" &&
327
+ value.encoding === "base64" &&
328
+ typeof value.data === "string" &&
329
+ typeof value.byteLength === "number" &&
330
+ typeof value.contentType === "string");
331
+ }
248
332
  function isQueryObject(value) {
249
333
  return value !== null && typeof value === "object" && !Array.isArray(value);
250
334
  }
@@ -13,5 +13,5 @@ export type { DefineClientOptions, DefinedClient, OpenApiClientServices } from "
13
13
  export type { AuthProvider, CommandContributor, TokenSource } from "./auth/types.js";
14
14
  export { bearerTokenAuth } from "./auth/bearer-token-auth.js";
15
15
  export type { BearerTokenAuthOptions } from "./auth/bearer-token-auth.js";
16
- export { HttpError, requestJson } from "./http.js";
16
+ export { HttpError, prepareMultipartFileInputs, requestJson, writeBinaryResponseOutput } from "./http.js";
17
17
  export type { BinaryHttpResponse, HttpErrorRequest, HttpErrorResponse, HttpRequestOptions, QueryValue } from "./http.js";
@@ -6,4 +6,4 @@ export { inspectOpenApiSource } from "./inspect-source.js";
6
6
  export { renderOpenApiInspection } from "./render-inspection.js";
7
7
  export { commandsFromSpec, defineClientFromSpec } from "./runtime.js";
8
8
  export { bearerTokenAuth } from "./auth/bearer-token-auth.js";
9
- export { HttpError, requestJson } from "./http.js";
9
+ export { HttpError, prepareMultipartFileInputs, requestJson, writeBinaryResponseOutput } from "./http.js";
@@ -3,7 +3,7 @@ import { defineCommand, defineGroup, S } from "toolcraft";
3
3
  import { defineClient } from "./define-client.js";
4
4
  import { collectSchemaOptionEntries, collectGeneratedCommands } from "./generate.js";
5
5
  import { groupByNoun } from "./group-by-noun.js";
6
- import { requestJson } from "./http.js";
6
+ import { prepareMultipartFileInputs, requestJson, writeBinaryResponseOutput } from "./http.js";
7
7
  import { buildRequestShape, executePreflightBlocks } from "./interpreter.js";
8
8
  import { parseOpenApiDocument, readOpenApiSourceText } from "./spec-source.js";
9
9
  const RUNTIME_COMMAND_SCOPE = ["cli", "mcp", "sdk"];
@@ -49,10 +49,16 @@ function createRuntimeCommand(command) {
49
49
  });
50
50
  }
51
51
  function createRuntimeHandler(command) {
52
- return async ({ params, baseUrl, tokenSource, fetch }) => {
52
+ return async ({ params, baseUrl, tokenSource, fetch, fs, env }) => {
53
53
  const resolvedValues = executePreflightBlocks(command.preflightBlocks, params);
54
54
  const requestShape = buildRequestShape(command.requestFields, command.sectionRenders, command.optionalSections, params, resolvedValues);
55
- return requestJson({
55
+ const preparedRequestShape = await prepareMultipartFileInputs(requestShape, {
56
+ bodyMode: command.bodyMode,
57
+ multipartBinaryFields: command.multipartBinaryFields,
58
+ fs,
59
+ env
60
+ });
61
+ const result = await requestJson({
56
62
  baseUrl: command.baseUrl ?? baseUrl,
57
63
  path: command.path,
58
64
  method: command.method,
@@ -66,8 +72,9 @@ function createRuntimeHandler(command) {
66
72
  fetch,
67
73
  dryRun: params.dryRun,
68
74
  verbose: params.verbose,
69
- ...requestShape
75
+ ...preparedRequestShape
70
76
  });
77
+ return writeBinaryResponseOutput(result, command.responseMode === "binary" ? params.output : undefined, { fs, env });
71
78
  };
72
79
  }
73
80
  function createRuntimeParamSchema(param) {