@utdk/cli 0.1.0-dev.646adf4
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +373 -0
- package/dist/auth.d.ts +24 -0
- package/dist/auth.js +111 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +368 -0
- package/dist/cli.js.map +1 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.js +111 -0
- package/dist/format.js.map +1 -0
- package/dist/providers.d.ts +70 -0
- package/dist/providers.js +317 -0
- package/dist/providers.js.map +1 -0
- package/package.json +32 -0
- package/src/auth.ts +123 -0
- package/src/bin.ts +11 -0
- package/src/cli.ts +451 -0
- package/src/format.ts +127 -0
- package/src/providers.ts +478 -0
- package/tsconfig.json +11 -0
package/src/providers.ts
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider and operation discovery for @utdk/cli.
|
|
3
|
+
*
|
|
4
|
+
* Scans the utdk package directory for providers and reads their OpenAPI
|
|
5
|
+
* specifications to enumerate operations. Name transformations mirror
|
|
6
|
+
* those in packages/utdk/client.ts so CLI operation names match the
|
|
7
|
+
* accessor paths on the created client.
|
|
8
|
+
*
|
|
9
|
+
* Also builds ToolRuntimeMetadataMap entries for path/query/header/body
|
|
10
|
+
* parameter routing, so the generated client can correctly assemble HTTP
|
|
11
|
+
* requests even when the provider's hand-written metadata.ts is empty.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
15
|
+
import { dirname, join, resolve } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import type { ToolRuntimeMetadata, ToolRuntimeMetadataMap } from "utdk/client";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
// Compiled to packages/utdk-cli/dist/; navigate to packages/utdk/
|
|
21
|
+
export const UTDK_ROOT = resolve(__dirname, "..", "..", "utdk");
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Types
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export type AuthConfig = {
|
|
28
|
+
auth_type: "api_key" | "oauth2" | "basic";
|
|
29
|
+
api_key?: string;
|
|
30
|
+
var_name?: string;
|
|
31
|
+
location?: string;
|
|
32
|
+
token_url?: string;
|
|
33
|
+
client_id?: string;
|
|
34
|
+
client_secret?: string;
|
|
35
|
+
scope?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ProviderInfo = {
|
|
39
|
+
name: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
auth: AuthConfig[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ParameterInfo = {
|
|
45
|
+
name: string;
|
|
46
|
+
in: "query" | "path" | "header" | "cookie";
|
|
47
|
+
required: boolean;
|
|
48
|
+
description?: string;
|
|
49
|
+
type?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type BodyInfo = {
|
|
53
|
+
kind: "none" | "properties" | "raw";
|
|
54
|
+
propertyKeys: string[];
|
|
55
|
+
contentType?: string;
|
|
56
|
+
allowsAdditionalProperties?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type OperationInfo = {
|
|
60
|
+
accessPath: string[];
|
|
61
|
+
operationId: string;
|
|
62
|
+
method: string;
|
|
63
|
+
path: string;
|
|
64
|
+
summary?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
parameters: ParameterInfo[];
|
|
67
|
+
body: BodyInfo;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Name transformation (mirrors client.ts identically)
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function sanitizeIdentifier(name: string): string {
|
|
75
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^[0-9]/, "_$&");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function splitIdentifierWords(name: string): string[] {
|
|
79
|
+
return name
|
|
80
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
81
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
82
|
+
.replace(/([A-Za-z])([0-9])/g, "$1 $2")
|
|
83
|
+
.replace(/([0-9])([A-Za-z])/g, "$1 $2")
|
|
84
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
85
|
+
.trim()
|
|
86
|
+
.split(/\s+/)
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function toCamelCase(name: string): string {
|
|
91
|
+
const cleaned = splitIdentifierWords(name);
|
|
92
|
+
if (cleaned.length === 0) return "_";
|
|
93
|
+
const [first = "_", ...rest] = cleaned;
|
|
94
|
+
return sanitizeIdentifier(
|
|
95
|
+
first.toLowerCase() +
|
|
96
|
+
rest.map((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()).join(""),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Converts an OpenAPI operationId to a client access path (same logic as client.ts). */
|
|
101
|
+
function operationIdToBasePath(operationId: string): string[] {
|
|
102
|
+
const segments = operationId.split("/").filter(Boolean);
|
|
103
|
+
const path = (segments.length > 0 ? segments : [operationId]).map(toCamelCase);
|
|
104
|
+
// Collapse duplicate consecutive leaf (e.g. a/a → ['a','call'])
|
|
105
|
+
if (path.length > 1 && path[path.length - 1] === path[path.length - 2]) {
|
|
106
|
+
path[path.length - 1] = "call";
|
|
107
|
+
}
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasPathConflict(candidate: string[], used: Set<string>): boolean {
|
|
112
|
+
const key = candidate.join(".");
|
|
113
|
+
for (const u of used) {
|
|
114
|
+
if (u === key || u.startsWith(`${key}.`) || key.startsWith(`${u}.`)) return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveAccessPath(basePath: string[], used: Set<string>): string[] {
|
|
120
|
+
const baseLeaf = basePath[basePath.length - 1] ?? "_";
|
|
121
|
+
let path = [...basePath];
|
|
122
|
+
let suffix = 2;
|
|
123
|
+
while (hasPathConflict(path, used)) {
|
|
124
|
+
path = [...basePath.slice(0, -1), `${baseLeaf}_${suffix}`];
|
|
125
|
+
suffix++;
|
|
126
|
+
}
|
|
127
|
+
used.add(path.join("."));
|
|
128
|
+
return path;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Provider listing
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/** Returns all provider names found in the utdk package directory. */
|
|
136
|
+
export function listProviders(): string[] {
|
|
137
|
+
if (!existsSync(UTDK_ROOT)) return [];
|
|
138
|
+
const entries = readdirSync(UTDK_ROOT, { withFileTypes: true });
|
|
139
|
+
return entries
|
|
140
|
+
.filter((e) => e.isDirectory() && existsSync(join(UTDK_ROOT, e.name, "openapi.json")))
|
|
141
|
+
.map((e) => e.name)
|
|
142
|
+
.sort();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Provider info
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
type ProviderPkg = {
|
|
150
|
+
description?: string;
|
|
151
|
+
utdk?: {
|
|
152
|
+
auth?: AuthConfig[];
|
|
153
|
+
openapi?: { title?: string };
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Returns metadata for a single provider, or undefined if not found. */
|
|
158
|
+
export function getProviderInfo(providerName: string): ProviderInfo | undefined {
|
|
159
|
+
const providerDir = join(UTDK_ROOT, providerName);
|
|
160
|
+
if (!existsSync(providerDir)) return undefined;
|
|
161
|
+
|
|
162
|
+
const pkgPath = join(providerDir, "package.json");
|
|
163
|
+
try {
|
|
164
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as ProviderPkg;
|
|
165
|
+
return {
|
|
166
|
+
name: providerName,
|
|
167
|
+
description: pkg.description ?? pkg.utdk?.openapi?.title,
|
|
168
|
+
auth: pkg.utdk?.auth ?? [],
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return { name: providerName, auth: [] };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// OpenAPI parsing helpers
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
type OpenApiSchema = {
|
|
180
|
+
type?: string;
|
|
181
|
+
properties?: Record<string, OpenApiSchema>;
|
|
182
|
+
additionalProperties?: boolean | OpenApiSchema;
|
|
183
|
+
$ref?: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
type OpenApiParameter = {
|
|
187
|
+
name: string;
|
|
188
|
+
in?: string;
|
|
189
|
+
required?: boolean;
|
|
190
|
+
description?: string;
|
|
191
|
+
schema?: OpenApiSchema;
|
|
192
|
+
$ref?: string;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
type OpenApiRequestBody = {
|
|
196
|
+
required?: boolean;
|
|
197
|
+
content?: Record<string, { schema?: OpenApiSchema | { $ref: string } }>;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
type OpenApiOperation = {
|
|
201
|
+
operationId?: string;
|
|
202
|
+
summary?: string;
|
|
203
|
+
description?: string;
|
|
204
|
+
parameters?: Array<OpenApiParameter | { $ref: string }>;
|
|
205
|
+
requestBody?: OpenApiRequestBody | { $ref: string };
|
|
206
|
+
// Swagger 2 body/formData live in parameters; handled implicitly above
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
type OpenApiPathItem = Record<string, OpenApiOperation | undefined>;
|
|
210
|
+
|
|
211
|
+
type OpenApiDoc = {
|
|
212
|
+
paths?: Record<string, OpenApiPathItem>;
|
|
213
|
+
components?: { schemas?: Record<string, OpenApiSchema> };
|
|
214
|
+
// Swagger 2
|
|
215
|
+
definitions?: Record<string, OpenApiSchema>;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
219
|
+
const JSON_CONTENT_TYPES = new Set([
|
|
220
|
+
"application/json",
|
|
221
|
+
"application/json-patch+json",
|
|
222
|
+
"application/merge-patch+json",
|
|
223
|
+
"application/vnd.api+json",
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
/** Resolve a JSON $ref to a schema object within the document. */
|
|
227
|
+
function resolveRef(doc: OpenApiDoc, ref: string): OpenApiSchema | undefined {
|
|
228
|
+
if (!ref.startsWith("#/")) return undefined;
|
|
229
|
+
const parts = ref.slice(2).split("/");
|
|
230
|
+
let current: unknown = doc;
|
|
231
|
+
for (const part of parts) {
|
|
232
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
233
|
+
current = (current as Record<string, unknown>)[part.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
234
|
+
}
|
|
235
|
+
return current as OpenApiSchema | undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveSchemaRef(doc: OpenApiDoc, schema: OpenApiSchema): OpenApiSchema {
|
|
239
|
+
if (schema.$ref) {
|
|
240
|
+
return resolveRef(doc, schema.$ref) ?? schema;
|
|
241
|
+
}
|
|
242
|
+
return schema;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Resolve a $ref parameter to a concrete OpenApiParameter, or return the input if already concrete. */
|
|
246
|
+
function resolveParameter(
|
|
247
|
+
doc: OpenApiDoc,
|
|
248
|
+
param: OpenApiParameter | { $ref: string },
|
|
249
|
+
): OpenApiParameter | undefined {
|
|
250
|
+
if (!("name" in param)) {
|
|
251
|
+
// It's a $ref
|
|
252
|
+
const resolved = resolveRef(doc, (param as { $ref: string }).$ref);
|
|
253
|
+
if (resolved && "name" in (resolved as object)) {
|
|
254
|
+
return resolved as unknown as OpenApiParameter;
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
return param;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Extract BodyInfo from an OpenAPI 3.x requestBody or Swagger 2 body param. */
|
|
262
|
+
function extractBodyInfo(
|
|
263
|
+
doc: OpenApiDoc,
|
|
264
|
+
operation: OpenApiOperation,
|
|
265
|
+
): BodyInfo {
|
|
266
|
+
// Check requestBody (OpenAPI 3.x)
|
|
267
|
+
if (operation.requestBody) {
|
|
268
|
+
let rb = operation.requestBody as OpenApiRequestBody | { $ref: string };
|
|
269
|
+
if ("$ref" in rb) {
|
|
270
|
+
const resolved = resolveRef(doc, rb.$ref);
|
|
271
|
+
if (resolved) rb = resolved as unknown as OpenApiRequestBody;
|
|
272
|
+
else return { kind: "none", propertyKeys: [] };
|
|
273
|
+
}
|
|
274
|
+
const content = (rb as OpenApiRequestBody).content;
|
|
275
|
+
if (!content) return { kind: "raw", propertyKeys: [] };
|
|
276
|
+
|
|
277
|
+
// Prefer JSON content types
|
|
278
|
+
const jsonEntry = Object.entries(content).find(([ct]) =>
|
|
279
|
+
JSON_CONTENT_TYPES.has(ct) || ct.includes("json"),
|
|
280
|
+
);
|
|
281
|
+
const [contentType, mediaType] = jsonEntry ?? Object.entries(content)[0] ?? [];
|
|
282
|
+
if (!contentType || !mediaType) return { kind: "none", propertyKeys: [] };
|
|
283
|
+
|
|
284
|
+
const rawSchema = mediaType.schema;
|
|
285
|
+
if (!rawSchema) return { kind: "raw", propertyKeys: [], contentType };
|
|
286
|
+
|
|
287
|
+
const schema = resolveSchemaRef(doc, rawSchema as OpenApiSchema);
|
|
288
|
+
if (schema.type === "object" || schema.properties) {
|
|
289
|
+
const keys = Object.keys(schema.properties ?? {});
|
|
290
|
+
const allowsAdditional =
|
|
291
|
+
schema.additionalProperties === true ||
|
|
292
|
+
(typeof schema.additionalProperties === "object" && schema.additionalProperties !== null);
|
|
293
|
+
return {
|
|
294
|
+
kind: "properties",
|
|
295
|
+
propertyKeys: keys,
|
|
296
|
+
contentType,
|
|
297
|
+
allowsAdditionalProperties: allowsAdditional || undefined,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return { kind: "raw", propertyKeys: [], contentType };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Swagger 2: check for body or formData parameters
|
|
304
|
+
const params = (operation.parameters ?? []) as Array<OpenApiParameter | { $ref: string }>;
|
|
305
|
+
const bodyParam = params.find(
|
|
306
|
+
(p): p is OpenApiParameter =>
|
|
307
|
+
"name" in p && ((p as OpenApiParameter).in === "body" || (p as OpenApiParameter).in === "formData"),
|
|
308
|
+
);
|
|
309
|
+
if (!bodyParam) return { kind: "none", propertyKeys: [] };
|
|
310
|
+
|
|
311
|
+
const schema = bodyParam.schema ? resolveSchemaRef(doc, bodyParam.schema) : undefined;
|
|
312
|
+
if (!schema) return { kind: "raw", propertyKeys: [], contentType: "application/json" };
|
|
313
|
+
|
|
314
|
+
if (schema.type === "object" || schema.properties) {
|
|
315
|
+
const keys = Object.keys(schema.properties ?? {});
|
|
316
|
+
return { kind: "properties", propertyKeys: keys, contentType: "application/json" };
|
|
317
|
+
}
|
|
318
|
+
return { kind: "raw", propertyKeys: [], contentType: "application/json" };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Operation listing
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
/** Parses the OpenAPI spec for a provider and returns all operations with their metadata. */
|
|
326
|
+
export function listOperations(providerName: string): OperationInfo[] {
|
|
327
|
+
const openApiPath = join(UTDK_ROOT, providerName, "openapi.json");
|
|
328
|
+
if (!existsSync(openApiPath)) return [];
|
|
329
|
+
|
|
330
|
+
let doc: OpenApiDoc;
|
|
331
|
+
try {
|
|
332
|
+
doc = JSON.parse(readFileSync(openApiPath, "utf-8")) as OpenApiDoc;
|
|
333
|
+
} catch {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const operations: OperationInfo[] = [];
|
|
338
|
+
const usedPaths = new Set<string>();
|
|
339
|
+
|
|
340
|
+
for (const [apiPath, pathItem] of Object.entries(doc.paths ?? {})) {
|
|
341
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
342
|
+
if (!HTTP_METHODS.has(method) || !operation) continue;
|
|
343
|
+
|
|
344
|
+
const operationId =
|
|
345
|
+
operation.operationId ??
|
|
346
|
+
`${method}${apiPath.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
347
|
+
|
|
348
|
+
const basePath = operationIdToBasePath(operationId);
|
|
349
|
+
const accessPath = resolveAccessPath(basePath, usedPaths);
|
|
350
|
+
|
|
351
|
+
// Merge path-item-level params (shared across all methods) with operation params.
|
|
352
|
+
// Operation params override path-item params with the same name.
|
|
353
|
+
const pathItemParams = (
|
|
354
|
+
(pathItem as Record<string, unknown>)["parameters"] as Array<
|
|
355
|
+
OpenApiParameter | { $ref: string }
|
|
356
|
+
> | undefined
|
|
357
|
+
) ?? [];
|
|
358
|
+
|
|
359
|
+
const rawParams = [
|
|
360
|
+
...pathItemParams,
|
|
361
|
+
...(operation.parameters ?? []),
|
|
362
|
+
] as Array<OpenApiParameter | { $ref: string }>;
|
|
363
|
+
|
|
364
|
+
// Deduplicate: operation-level params override path-level params of the same name.
|
|
365
|
+
const seen = new Set<string>();
|
|
366
|
+
const dedupedParams: Array<OpenApiParameter | { $ref: string }> = [];
|
|
367
|
+
// Process operation params first (higher priority), then path params.
|
|
368
|
+
const operationParams = (operation.parameters ?? []) as Array<
|
|
369
|
+
OpenApiParameter | { $ref: string }
|
|
370
|
+
>;
|
|
371
|
+
for (const p of [...operationParams, ...pathItemParams]) {
|
|
372
|
+
const resolved = resolveParameter(doc, p);
|
|
373
|
+
if (!resolved) continue;
|
|
374
|
+
if (!seen.has(resolved.name)) {
|
|
375
|
+
seen.add(resolved.name);
|
|
376
|
+
dedupedParams.push(resolved);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const parameters: ParameterInfo[] = dedupedParams
|
|
381
|
+
.map((p) => resolveParameter(doc, p))
|
|
382
|
+
.filter(
|
|
383
|
+
(p): p is OpenApiParameter =>
|
|
384
|
+
p !== undefined && p.in !== "body" && p.in !== "formData",
|
|
385
|
+
)
|
|
386
|
+
.map((p) => ({
|
|
387
|
+
name: p.name,
|
|
388
|
+
in: (p.in as ParameterInfo["in"]) ?? "query",
|
|
389
|
+
required: p.required ?? false,
|
|
390
|
+
description: p.description,
|
|
391
|
+
type: p.schema?.type,
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
void rawParams; // used above via pathItemParams/operationParams
|
|
395
|
+
|
|
396
|
+
const body = extractBodyInfo(doc, operation);
|
|
397
|
+
|
|
398
|
+
operations.push({
|
|
399
|
+
accessPath,
|
|
400
|
+
operationId,
|
|
401
|
+
method: method.toUpperCase(),
|
|
402
|
+
path: apiPath,
|
|
403
|
+
summary: operation.summary,
|
|
404
|
+
description: operation.description,
|
|
405
|
+
parameters,
|
|
406
|
+
body,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return operations;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Returns the OperationInfo for a specific access-path string like "users.getByUsername". */
|
|
415
|
+
export function findOperation(
|
|
416
|
+
providerName: string,
|
|
417
|
+
operationPath: string,
|
|
418
|
+
): OperationInfo | undefined {
|
|
419
|
+
const ops = listOperations(providerName);
|
|
420
|
+
return ops.find(
|
|
421
|
+
(op) =>
|
|
422
|
+
op.accessPath.join(".") === operationPath ||
|
|
423
|
+
op.accessPath.join(".").toLowerCase() === operationPath.toLowerCase() ||
|
|
424
|
+
op.operationId === operationPath,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Tool metadata builder
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Build a ToolRuntimeMetadataMap from the provider's OpenAPI spec.
|
|
434
|
+
*
|
|
435
|
+
* The map keys equal the bare operationId (which is what OpenApiConverter
|
|
436
|
+
* uses as tool.name). Providing this map to createClient() gives the
|
|
437
|
+
* request assembler correct path/query/header/body routing without
|
|
438
|
+
* relying on the provider's (often empty) hand-written metadata.ts.
|
|
439
|
+
*/
|
|
440
|
+
export function buildToolMetadata(ops: OperationInfo[]): ToolRuntimeMetadataMap {
|
|
441
|
+
const map: ToolRuntimeMetadataMap = {};
|
|
442
|
+
|
|
443
|
+
for (const op of ops) {
|
|
444
|
+
// tool.name from OpenApiConverter equals the bare operationId (no provider prefix).
|
|
445
|
+
const toolName = op.operationId;
|
|
446
|
+
|
|
447
|
+
const pathKeys = op.parameters
|
|
448
|
+
.filter((p) => p.in === "path")
|
|
449
|
+
.map((p) => p.name);
|
|
450
|
+
const queryKeys = op.parameters
|
|
451
|
+
.filter((p) => p.in === "query")
|
|
452
|
+
.map((p) => p.name);
|
|
453
|
+
const headerKeys = op.parameters
|
|
454
|
+
.filter((p) => p.in === "header")
|
|
455
|
+
.map((p) => p.name);
|
|
456
|
+
|
|
457
|
+
const meta: ToolRuntimeMetadata = {
|
|
458
|
+
accessPath: op.accessPath,
|
|
459
|
+
bodyKind: op.body.kind,
|
|
460
|
+
bodyPropertyKeys: op.body.propertyKeys,
|
|
461
|
+
...(op.body.allowsAdditionalProperties
|
|
462
|
+
? { bodyAllowsAdditionalProperties: true }
|
|
463
|
+
: {}),
|
|
464
|
+
...(op.body.contentType ? { contentType: op.body.contentType } : {}),
|
|
465
|
+
headerParameterKeys: headerKeys,
|
|
466
|
+
method: op.method,
|
|
467
|
+
routeTemplate: op.path,
|
|
468
|
+
pathConflictKeys: [],
|
|
469
|
+
pathParameterKeys: pathKeys,
|
|
470
|
+
queryConflictKeys: [],
|
|
471
|
+
queryParameterKeys: queryKeys,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
map[toolName] = meta;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return map;
|
|
478
|
+
}
|