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.
- package/dist/cli/commands/gaslight.js +50 -15
- package/dist/cli/commands/gaslight.js.map +1 -1
- package/dist/index.js +118 -46
- package/dist/index.js.map +3 -3
- package/dist/metafile.json +1 -1
- package/dist/sdk/gaslight.d.ts +1 -1
- package/dist/sdk/gaslight.js.map +1 -1
- package/package.json +1 -1
- package/packages/agent-gaslight/dist/index.d.ts +1 -1
- package/packages/agent-gaslight/dist/run.js +74 -28
- package/packages/agent-gaslight/dist/types.d.ts +13 -1
- package/packages/toolcraft-openapi/dist/generate.js +58 -8
- package/packages/toolcraft-openapi/dist/http.d.ts +14 -0
- package/packages/toolcraft-openapi/dist/http.js +85 -1
- package/packages/toolcraft-openapi/dist/index.d.ts +1 -1
- package/packages/toolcraft-openapi/dist/index.js +1 -1
- package/packages/toolcraft-openapi/dist/runtime.js +11 -4
package/dist/sdk/gaslight.d.ts
CHANGED
|
@@ -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";
|
package/dist/sdk/gaslight.js.map
CHANGED
|
@@ -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,
|
|
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,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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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, ...
|
|
227
|
-
const params = [...operationParams.params, ...qualifiedRequestBodyParams.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";',
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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) {
|