openpets 1.0.10 → 1.0.12

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 (72) hide show
  1. package/dist/data/api.json +3758 -7222
  2. package/dist/src/core/build-pet.d.ts.map +1 -1
  3. package/dist/src/core/build-pet.js +7 -0
  4. package/dist/src/core/build-pet.js.map +1 -1
  5. package/dist/src/core/cli.js +456 -130
  6. package/dist/src/core/cli.js.map +1 -1
  7. package/dist/src/core/ensure-npmignore.d.ts +30 -0
  8. package/dist/src/core/ensure-npmignore.d.ts.map +1 -0
  9. package/dist/src/core/ensure-npmignore.js +121 -0
  10. package/dist/src/core/ensure-npmignore.js.map +1 -0
  11. package/dist/src/core/index.d.ts +6 -3
  12. package/dist/src/core/index.d.ts.map +1 -1
  13. package/dist/src/core/index.js +9 -3
  14. package/dist/src/core/index.js.map +1 -1
  15. package/dist/src/core/mcp-generator.d.ts +56 -0
  16. package/dist/src/core/mcp-generator.d.ts.map +1 -0
  17. package/dist/src/core/mcp-generator.js +1438 -0
  18. package/dist/src/core/mcp-generator.js.map +1 -0
  19. package/dist/src/core/mcp-server.js +0 -0
  20. package/dist/src/core/openapi-generator.d.ts +59 -0
  21. package/dist/src/core/openapi-generator.d.ts.map +1 -0
  22. package/dist/src/core/openapi-generator.js +800 -0
  23. package/dist/src/core/openapi-generator.js.map +1 -0
  24. package/dist/src/core/pet-config.d.ts +107 -49
  25. package/dist/src/core/pet-config.d.ts.map +1 -1
  26. package/dist/src/core/pet-config.js +6 -4
  27. package/dist/src/core/pet-config.js.map +1 -1
  28. package/dist/src/core/pet-downloader.d.ts +16 -0
  29. package/dist/src/core/pet-downloader.d.ts.map +1 -1
  30. package/dist/src/core/pet-downloader.js +145 -3
  31. package/dist/src/core/pet-downloader.js.map +1 -1
  32. package/dist/src/core/publish-pet.d.ts +29 -0
  33. package/dist/src/core/publish-pet.d.ts.map +1 -0
  34. package/dist/src/core/publish-pet.js +372 -0
  35. package/dist/src/core/publish-pet.js.map +1 -0
  36. package/dist/src/core/sdk-generator.d.ts +92 -0
  37. package/dist/src/core/sdk-generator.d.ts.map +1 -0
  38. package/dist/src/core/sdk-generator.js +567 -0
  39. package/dist/src/core/sdk-generator.js.map +1 -0
  40. package/dist/src/core/search-pets.d.ts +5 -0
  41. package/dist/src/core/search-pets.d.ts.map +1 -1
  42. package/dist/src/core/search-pets.js +43 -0
  43. package/dist/src/core/search-pets.js.map +1 -1
  44. package/dist/src/core/security-scanner.d.ts +49 -0
  45. package/dist/src/core/security-scanner.d.ts.map +1 -0
  46. package/dist/src/core/security-scanner.js +255 -0
  47. package/dist/src/core/security-scanner.js.map +1 -0
  48. package/dist/src/core/tool-lister.d.ts +61 -0
  49. package/dist/src/core/tool-lister.d.ts.map +1 -0
  50. package/dist/src/core/tool-lister.js +333 -0
  51. package/dist/src/core/tool-lister.js.map +1 -0
  52. package/dist/src/core/validate-pet.d.ts +2 -0
  53. package/dist/src/core/validate-pet.d.ts.map +1 -1
  54. package/dist/src/core/validate-pet.js +93 -1
  55. package/dist/src/core/validate-pet.js.map +1 -1
  56. package/dist/src/sdk/plugin-factory.d.ts +86 -0
  57. package/dist/src/sdk/plugin-factory.d.ts.map +1 -1
  58. package/dist/src/sdk/plugin-factory.js +450 -53
  59. package/dist/src/sdk/plugin-factory.js.map +1 -1
  60. package/dist/src/sdk/prompts-manager.d.ts +6 -0
  61. package/dist/src/sdk/prompts-manager.d.ts.map +1 -0
  62. package/dist/src/sdk/prompts-manager.js +162 -0
  63. package/dist/src/sdk/prompts-manager.js.map +1 -0
  64. package/package.json +1 -1
  65. package/dist/src/core/local-cache.d.ts +0 -69
  66. package/dist/src/core/local-cache.d.ts.map +0 -1
  67. package/dist/src/core/local-cache.js +0 -212
  68. package/dist/src/core/local-cache.js.map +0 -1
  69. package/dist/src/core/plugin-factory.d.ts +0 -58
  70. package/dist/src/core/plugin-factory.d.ts.map +0 -1
  71. package/dist/src/core/plugin-factory.js +0 -212
  72. package/dist/src/core/plugin-factory.js.map +0 -1
@@ -0,0 +1,800 @@
1
+ /**
2
+ * OpenAPI Tool Generator
3
+ *
4
+ * Generates OpenPets-compatible tool definitions from OpenAPI/Swagger specifications.
5
+ * This is the highest-level way to create tools for API-first products.
6
+ *
7
+ * Usage:
8
+ * pets generate-openapi --url https://api.example.com/openapi.json
9
+ * pets generate-openapi --file ./openapi.json
10
+ * pets generate-openapi # Uses openapiSpec.url from package.json
11
+ *
12
+ * This tool:
13
+ * 1. Fetches or reads an OpenAPI spec (v3.0 or v3.1)
14
+ * 2. Parses endpoints into tool definitions
15
+ * 3. Generates Zod schemas from JSON Schema
16
+ * 4. Creates an openapi-client.ts file with all tools
17
+ */
18
+ import { existsSync, readFileSync, writeFileSync } from "fs";
19
+ import { resolve, basename } from "path";
20
+ import { config as loadDotenv } from "dotenv";
21
+ import { createLogger } from "./logger";
22
+ const logger = createLogger("openapi-generator");
23
+ // ============================================================================
24
+ // Config Loading
25
+ // ============================================================================
26
+ function loadConfigFromPackageJson(dir) {
27
+ const packageJsonPath = resolve(dir, "package.json");
28
+ if (!existsSync(packageJsonPath)) {
29
+ return null;
30
+ }
31
+ try {
32
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
33
+ // Check for explicit openapiSpec config
34
+ if (packageJson.openapiSpec) {
35
+ const petName = packageJson.name?.replace(/^@openpets\//, "") || basename(dir);
36
+ return {
37
+ config: packageJson.openapiSpec,
38
+ petName,
39
+ };
40
+ }
41
+ // Check for openapi-config.json in the pet directory
42
+ const configPath = resolve(dir, "openapi-config.json");
43
+ if (existsSync(configPath)) {
44
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
45
+ const petName = packageJson.name?.replace(/^@openpets\//, "") || basename(dir);
46
+ return {
47
+ config: config,
48
+ petName,
49
+ };
50
+ }
51
+ return null;
52
+ }
53
+ catch (error) {
54
+ logger.error(`Error reading package.json: ${error.message}`);
55
+ return null;
56
+ }
57
+ }
58
+ // ============================================================================
59
+ // OpenAPI Spec Fetching
60
+ // ============================================================================
61
+ async function fetchOpenAPISpec(url) {
62
+ const response = await fetch(url);
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`);
65
+ }
66
+ return response.json();
67
+ }
68
+ function readOpenAPISpec(filePath) {
69
+ const content = readFileSync(filePath, "utf-8");
70
+ return JSON.parse(content);
71
+ }
72
+ // ============================================================================
73
+ // Schema Resolution
74
+ // ============================================================================
75
+ function resolveRef(spec, ref) {
76
+ // Handle $ref like "#/components/schemas/UserLogin"
77
+ const path = ref.replace(/^#\//, "").split("/");
78
+ let current = spec;
79
+ for (const segment of path) {
80
+ current = current[segment];
81
+ if (!current) {
82
+ logger.warn(`Could not resolve ref: ${ref}`);
83
+ return { type: "object" };
84
+ }
85
+ }
86
+ return current;
87
+ }
88
+ function resolveSchema(spec, schema) {
89
+ if (!schema)
90
+ return { type: "object" };
91
+ if (schema.$ref) {
92
+ return resolveSchema(spec, resolveRef(spec, schema.$ref));
93
+ }
94
+ // Handle allOf by merging schemas
95
+ if (schema.allOf) {
96
+ const merged = { type: "object", properties: {}, required: [] };
97
+ for (const subSchema of schema.allOf) {
98
+ const resolved = resolveSchema(spec, subSchema);
99
+ if (resolved.properties) {
100
+ merged.properties = { ...merged.properties, ...resolved.properties };
101
+ }
102
+ if (resolved.required) {
103
+ merged.required = [...(merged.required || []), ...resolved.required];
104
+ }
105
+ }
106
+ return merged;
107
+ }
108
+ return schema;
109
+ }
110
+ // ============================================================================
111
+ // Zod Schema Generation
112
+ // ============================================================================
113
+ function schemaToZod(spec, schema, depth = 0) {
114
+ if (!schema)
115
+ return "z.any()";
116
+ // Resolve refs
117
+ const resolved = resolveSchema(spec, schema);
118
+ const desc = resolved.description
119
+ ? `.describe(${JSON.stringify(resolved.description)})`
120
+ : "";
121
+ // Handle anyOf (nullable/union types)
122
+ if (resolved.anyOf && Array.isArray(resolved.anyOf)) {
123
+ const nonNullTypes = resolved.anyOf.filter(s => s.type !== "null");
124
+ if (nonNullTypes.length === 1) {
125
+ return schemaToZod(spec, { ...nonNullTypes[0], description: resolved.description }, depth);
126
+ }
127
+ // Multiple types - use any with description
128
+ return `z.any()${desc}`;
129
+ }
130
+ // Handle oneOf similarly
131
+ if (resolved.oneOf && Array.isArray(resolved.oneOf)) {
132
+ const types = resolved.oneOf.filter(s => s.type !== "null");
133
+ if (types.length === 1) {
134
+ return schemaToZod(spec, { ...types[0], description: resolved.description }, depth);
135
+ }
136
+ return `z.any()${desc}`;
137
+ }
138
+ switch (resolved.type) {
139
+ case "string":
140
+ if (resolved.enum && resolved.enum.length > 0) {
141
+ const enumVals = resolved.enum.slice(0, 10).map(v => JSON.stringify(v)).join(", ");
142
+ const more = resolved.enum.length > 10 ? ` /* +${resolved.enum.length - 10} more */` : "";
143
+ return `z.enum([${enumVals}${more}])${desc}`;
144
+ }
145
+ if (resolved.format === "email") {
146
+ return `z.string().email()${desc}`;
147
+ }
148
+ if (resolved.format === "date-time") {
149
+ return `z.string()${desc.replace('.describe("', '.describe("ISO 8601 datetime: ')}`;
150
+ }
151
+ return `z.string()${desc}`;
152
+ case "number":
153
+ case "integer":
154
+ return `z.number()${desc}`;
155
+ case "boolean":
156
+ return `z.boolean()${desc}`;
157
+ case "array":
158
+ if (depth > 1) {
159
+ // OpenCode limitation: nested arrays become JSON strings
160
+ const arrayDesc = resolved.description
161
+ ? `.describe(${JSON.stringify("JSON array: " + resolved.description)})`
162
+ : '.describe("JSON array")';
163
+ return `z.string()${arrayDesc}`;
164
+ }
165
+ if (resolved.items) {
166
+ const itemsResolved = resolveSchema(spec, resolved.items);
167
+ if (itemsResolved.type === "string") {
168
+ return `z.array(z.string())${desc}`;
169
+ }
170
+ if (itemsResolved.type === "number" || itemsResolved.type === "integer") {
171
+ return `z.array(z.number())${desc}`;
172
+ }
173
+ if (itemsResolved.enum) {
174
+ const enumVals = itemsResolved.enum.slice(0, 10).map((v) => JSON.stringify(v)).join(", ");
175
+ return `z.array(z.enum([${enumVals}]))${desc}`;
176
+ }
177
+ }
178
+ // Complex array items - use JSON string
179
+ const arrayDesc = resolved.description
180
+ ? `.describe(${JSON.stringify("JSON array: " + resolved.description)})`
181
+ : '.describe("JSON array")';
182
+ return `z.string()${arrayDesc}`;
183
+ case "object":
184
+ if (depth > 0) {
185
+ // Nested objects become JSON strings (OpenCode limitation)
186
+ const objDesc = generateObjectDescription(spec, resolved);
187
+ return `z.string().describe(${JSON.stringify("JSON object: " + objDesc)})`;
188
+ }
189
+ // Top-level objects can be expanded
190
+ if (resolved.properties && Object.keys(resolved.properties).length > 0) {
191
+ const props = Object.entries(resolved.properties).map(([name, propSchema]) => {
192
+ const isRequired = resolved.required?.includes(name);
193
+ let zodType = schemaToZod(spec, propSchema, depth + 1);
194
+ if (!isRequired && !zodType.includes(".optional()")) {
195
+ zodType += ".optional()";
196
+ }
197
+ return ` ${name}: ${zodType}`;
198
+ });
199
+ return `z.object({\n${props.join(",\n")}\n })`;
200
+ }
201
+ return `z.any()${desc}`;
202
+ default:
203
+ return `z.any()${desc}`;
204
+ }
205
+ }
206
+ function generateObjectDescription(spec, schema) {
207
+ const parts = [];
208
+ if (schema.description) {
209
+ parts.push(schema.description);
210
+ }
211
+ else if (schema.title) {
212
+ parts.push(schema.title);
213
+ }
214
+ if (schema.properties) {
215
+ const required = schema.required || [];
216
+ const propDescs = [];
217
+ for (const [name, propSchema] of Object.entries(schema.properties)) {
218
+ const resolved = resolveSchema(spec, propSchema);
219
+ const isRequired = required.includes(name);
220
+ const reqMarker = isRequired ? " (required)" : "";
221
+ let typeDesc = resolved.type || "any";
222
+ if (resolved.enum) {
223
+ typeDesc = `enum: [${resolved.enum.slice(0, 3).join(", ")}${resolved.enum.length > 3 ? "..." : ""}]`;
224
+ }
225
+ let desc = `${name}${reqMarker}: ${typeDesc}`;
226
+ if (resolved.description) {
227
+ desc += ` - ${resolved.description.substring(0, 50)}${resolved.description.length > 50 ? "..." : ""}`;
228
+ }
229
+ propDescs.push(desc);
230
+ }
231
+ if (propDescs.length > 0) {
232
+ parts.push("Properties: " + propDescs.slice(0, 5).join("; "));
233
+ if (propDescs.length > 5) {
234
+ parts.push(`... and ${propDescs.length - 5} more`);
235
+ }
236
+ }
237
+ }
238
+ return parts.join(". ") || "JSON object";
239
+ }
240
+ // ============================================================================
241
+ // Tool Generation
242
+ // ============================================================================
243
+ function operationToTool(spec, path, method, operation, pathParams, petName, readOnlyPatterns, coreEndpoints) {
244
+ // Generate tool name from operationId or path
245
+ let toolName;
246
+ if (operation.operationId) {
247
+ // Convert operationId to kebab-case with pet prefix
248
+ toolName = `${petName}-${operation.operationId
249
+ .replace(/([A-Z])/g, "-$1")
250
+ .replace(/_/g, "-")
251
+ .replace(/--+/g, "-")
252
+ .replace(/^-/, "")
253
+ .toLowerCase()}`;
254
+ }
255
+ else {
256
+ // Generate from method + path
257
+ const pathParts = path.replace(/\{[^}]+\}/g, "").split("/").filter(Boolean);
258
+ toolName = `${petName}-${method}-${pathParts.join("-")}`.toLowerCase();
259
+ }
260
+ // Description from summary/description
261
+ const description = operation.summary || operation.description || `${method.toUpperCase()} ${path}`;
262
+ // Combine path-level and operation-level parameters
263
+ const allParams = [...pathParams, ...(operation.parameters || [])];
264
+ // Convert parameters to our format
265
+ const parameters = allParams.map(param => {
266
+ const paramSchema = resolveSchema(spec, param.schema);
267
+ return {
268
+ name: param.name,
269
+ in: param.in,
270
+ required: param.required || param.in === "path",
271
+ schema: schemaToZod(spec, paramSchema),
272
+ description: param.description || paramSchema.description,
273
+ };
274
+ });
275
+ // Handle request body - expand properties into individual parameters
276
+ let hasRequestBody = false;
277
+ if (operation.requestBody?.content) {
278
+ const jsonContent = operation.requestBody.content["application/json"];
279
+ if (jsonContent?.schema) {
280
+ hasRequestBody = true;
281
+ const bodySchema = resolveSchema(spec, jsonContent.schema);
282
+ const bodyRequired = operation.requestBody.required ?? false;
283
+ // Expand body properties into individual parameters with "body" location
284
+ if (bodySchema.properties && Object.keys(bodySchema.properties).length > 0) {
285
+ const requiredProps = bodySchema.required || [];
286
+ for (const [propName, propSchema] of Object.entries(bodySchema.properties)) {
287
+ const resolvedProp = resolveSchema(spec, propSchema);
288
+ const isRequired = bodyRequired && requiredProps.includes(propName);
289
+ parameters.push({
290
+ name: propName,
291
+ in: "body",
292
+ required: isRequired,
293
+ schema: schemaToZod(spec, resolvedProp, 0),
294
+ description: resolvedProp.description || resolvedProp.title,
295
+ });
296
+ }
297
+ }
298
+ }
299
+ }
300
+ // Determine if read-only
301
+ const isReadOnly = method === "get" || method === "head" || method === "options" ||
302
+ readOnlyPatterns.some(pattern => toolName.includes(pattern) || operation.operationId?.includes(pattern));
303
+ // Determine if this is a core tool (always loaded) or requires env var
304
+ const tags = operation.tags || [];
305
+ let isCore = true; // Default to core if no coreEndpoints config
306
+ let loadEnvVar;
307
+ if (coreEndpoints) {
308
+ // Check if this tool matches any core endpoint criteria
309
+ const matchesCoreTag = coreEndpoints.tags?.some(tag => tags.map(t => t.toLowerCase()).includes(tag.toLowerCase()));
310
+ const matchesCorePath = coreEndpoints.paths?.some(pattern => path.toLowerCase().startsWith(pattern.toLowerCase()));
311
+ const matchesCoreOperationId = coreEndpoints.operationIds?.some(opId => operation.operationId?.toLowerCase().includes(opId.toLowerCase()));
312
+ isCore = !!(matchesCoreTag || matchesCorePath || matchesCoreOperationId);
313
+ if (!isCore) {
314
+ // Generate env var name for loading this tool group
315
+ // e.g., AI_HARMONY_LOAD_ADMIN_TOOLS=true
316
+ const tagName = (tags[0] || 'OTHER').replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
317
+ loadEnvVar = `${petName.toUpperCase().replace(/-/g, '_')}_LOAD_${tagName}_TOOLS`;
318
+ }
319
+ }
320
+ return {
321
+ name: toolName,
322
+ method: method.toUpperCase(),
323
+ path,
324
+ description,
325
+ parameters,
326
+ hasRequestBody,
327
+ isReadOnly,
328
+ tags,
329
+ isCore,
330
+ loadEnvVar,
331
+ };
332
+ }
333
+ function parseOpenAPISpec(spec, petName, config) {
334
+ const tools = [];
335
+ const readOnlyPatterns = config.readOnlyPatterns || ["list", "get", "search", "check", "status", "health"];
336
+ for (const [path, pathItem] of Object.entries(spec.paths)) {
337
+ const pathParams = pathItem.parameters || [];
338
+ const methods = [
339
+ { method: "get", operation: pathItem.get },
340
+ { method: "post", operation: pathItem.post },
341
+ { method: "put", operation: pathItem.put },
342
+ { method: "patch", operation: pathItem.patch },
343
+ { method: "delete", operation: pathItem.delete },
344
+ ];
345
+ for (const { method, operation } of methods) {
346
+ if (!operation)
347
+ continue;
348
+ // Skip deprecated operations
349
+ if (operation.deprecated)
350
+ continue;
351
+ // Check include/exclude filters
352
+ if (config.include && config.include.length > 0) {
353
+ const matches = config.include.some(pattern => operation.operationId?.includes(pattern) || path.includes(pattern));
354
+ if (!matches)
355
+ continue;
356
+ }
357
+ if (config.exclude && config.exclude.length > 0) {
358
+ const matches = config.exclude.some(pattern => operation.operationId?.includes(pattern) || path.includes(pattern));
359
+ if (matches)
360
+ continue;
361
+ }
362
+ const tool = operationToTool(spec, path, method, operation, pathParams, petName, readOnlyPatterns, config.coreEndpoints);
363
+ tools.push(tool);
364
+ }
365
+ }
366
+ return tools;
367
+ }
368
+ // ============================================================================
369
+ // File Generation
370
+ // ============================================================================
371
+ /**
372
+ * Check if a property name needs to be quoted in JavaScript
373
+ */
374
+ function needsQuotes(name) {
375
+ // Property names with special characters need quotes
376
+ return /[^a-zA-Z0-9_$]/.test(name) || /^\d/.test(name);
377
+ }
378
+ /**
379
+ * Get safe property accessor for args object
380
+ */
381
+ function getPropertyAccessor(name) {
382
+ return needsQuotes(name) ? `args["${name}"]` : `args.${name}`;
383
+ }
384
+ /**
385
+ * Get safe property name for object literals
386
+ */
387
+ function getPropertyName(name) {
388
+ return needsQuotes(name) ? `"${name}"` : name;
389
+ }
390
+ function generateToolDefinition(tool) {
391
+ // Build schema fields
392
+ const schemaFields = [];
393
+ // Add parameters (path, query, header, body)
394
+ for (const param of tool.parameters) {
395
+ let zodType = param.schema;
396
+ // Add description if not already present
397
+ if (param.description && !zodType.includes(".describe(")) {
398
+ // Insert .describe() before .optional() if present, otherwise append
399
+ if (zodType.includes(".optional()")) {
400
+ zodType = zodType.replace(".optional()", `.describe(${JSON.stringify(param.description)}).optional()`);
401
+ }
402
+ else {
403
+ zodType = zodType + `.describe(${JSON.stringify(param.description)})`;
404
+ }
405
+ }
406
+ // Add optional if not required and not already optional
407
+ if (!param.required && !zodType.includes(".optional()")) {
408
+ zodType += ".optional()";
409
+ }
410
+ // Use quoted property name if needed
411
+ const propName = getPropertyName(param.name);
412
+ schemaFields.push(` ${propName}: ${zodType}`);
413
+ }
414
+ const schemaBody = schemaFields.length > 0
415
+ ? `z.object({\n${schemaFields.join(",\n")}\n })`
416
+ : "z.object({})";
417
+ // Generate path with parameter substitution
418
+ const pathWithParams = tool.path.replace(/\{([^}]+)\}/g, (_, name) => `\${${getPropertyAccessor(name)}}`);
419
+ // Generate execute body
420
+ const pathParams = tool.parameters.filter(p => p.in === "path").map(p => p.name);
421
+ const queryParams = tool.parameters.filter(p => p.in === "query").map(p => p.name);
422
+ const headerParams = tool.parameters.filter(p => p.in === "header").map(p => p.name);
423
+ const bodyParams = tool.parameters.filter(p => p.in === "body").map(p => p.name);
424
+ let executeBody = ` const url = \`\${baseUrl}${pathWithParams}\``;
425
+ // Add query parameters
426
+ if (queryParams.length > 0) {
427
+ executeBody += `
428
+ const params = new URLSearchParams()
429
+ ${queryParams.map(p => ` if (${getPropertyAccessor(p)} !== undefined) params.append("${p}", String(${getPropertyAccessor(p)}))`).join("\n")}
430
+ const queryString = params.toString()
431
+ const fullUrl = queryString ? \`\${url}?\${queryString}\` : url`;
432
+ }
433
+ else {
434
+ executeBody += `
435
+ const fullUrl = url`;
436
+ }
437
+ // Build fetch options
438
+ executeBody += `
439
+
440
+ const options: RequestInit = {
441
+ method: "${tool.method}",
442
+ headers: { ...headers }`;
443
+ // Add header parameters
444
+ if (headerParams.length > 0) {
445
+ for (const param of headerParams) {
446
+ executeBody += `,
447
+ ...(${getPropertyAccessor(param)} ? { "${param}": ${getPropertyAccessor(param)} } : {})`;
448
+ }
449
+ }
450
+ // Add body from body parameters
451
+ if (tool.hasRequestBody && bodyParams.length > 0) {
452
+ executeBody += `,
453
+ body: JSON.stringify({
454
+ ${bodyParams.map(p => ` ...(${getPropertyAccessor(p)} !== undefined ? { ${getPropertyName(p)}: ${getPropertyAccessor(p)} } : {})`).join(",\n")}
455
+ })`;
456
+ }
457
+ executeBody += `
458
+ }
459
+
460
+ return fetchAPI(fullUrl, options)`;
461
+ return ` {
462
+ name: "${tool.name}",
463
+ description: ${JSON.stringify(tool.description)},
464
+ schema: ${schemaBody},
465
+ async execute(args: any) {
466
+ ${executeBody}
467
+ }
468
+ }`;
469
+ }
470
+ function generateOpenAPIClientFile(spec, tools, config, petName) {
471
+ // Separate core tools from loadEnv tools
472
+ const coreTools = tools.filter(t => t.isCore);
473
+ const loadEnvTools = tools.filter(t => !t.isCore);
474
+ // Group loadEnv tools by their env var
475
+ const loadEnvGroups = {};
476
+ for (const tool of loadEnvTools) {
477
+ const envVar = tool.loadEnvVar || 'OTHER';
478
+ if (!loadEnvGroups[envVar])
479
+ loadEnvGroups[envVar] = [];
480
+ loadEnvGroups[envVar].push(tool);
481
+ }
482
+ const coreToolDefinitions = coreTools.map(generateToolDefinition).join(",\n\n");
483
+ // Generate loadEnv tool groups
484
+ const loadEnvGroupDefinitions = [];
485
+ for (const [envVar, groupTools] of Object.entries(loadEnvGroups)) {
486
+ const groupDefs = groupTools.map(generateToolDefinition).join(",\n\n");
487
+ loadEnvGroupDefinitions.push(`
488
+ // ${envVar} tools (${groupTools.length} tools)
489
+ // Enable with: ${envVar}=true
490
+ const ${envVar.toLowerCase().replace(/_/g, '')}Tools: ToolDefinition[] = process.env.${envVar} === 'true' ? [
491
+ ${groupDefs}
492
+ ] : []
493
+ `);
494
+ }
495
+ // Determine base URL
496
+ // Priority: config.baseUrl > spec.servers[0].url > derived from spec URL
497
+ let baseUrl = config.baseUrl || spec.servers?.[0]?.url || "";
498
+ // If still empty and we have a URL, derive baseUrl from it
499
+ if (!baseUrl && config.url) {
500
+ try {
501
+ const specUrl = new URL(config.url);
502
+ baseUrl = `${specUrl.protocol}//${specUrl.host}`;
503
+ }
504
+ catch {
505
+ // Ignore URL parsing errors
506
+ }
507
+ }
508
+ // Determine auth setup
509
+ const authEnvVar = config.authEnvVar || `${petName.toUpperCase().replace(/-/g, "_")}_API_KEY`;
510
+ const authType = config.authType || "bearer";
511
+ const apiKeyHeader = config.apiKeyHeader || "X-API-Key";
512
+ let authSetup;
513
+ switch (authType) {
514
+ case "apiKey":
515
+ authSetup = `if (apiKey) {
516
+ headers["${apiKeyHeader}"] = apiKey
517
+ }`;
518
+ break;
519
+ case "basic":
520
+ authSetup = `if (apiKey) {
521
+ headers["Authorization"] = \`Basic \${apiKey}\`
522
+ }`;
523
+ break;
524
+ case "none":
525
+ authSetup = "";
526
+ break;
527
+ default: // bearer
528
+ authSetup = `if (apiKey) {
529
+ headers["Authorization"] = \`Bearer \${apiKey}\`
530
+ }`;
531
+ }
532
+ // Count read-only vs write tools
533
+ const readOnlyCount = tools.filter(t => t.isReadOnly).length;
534
+ const writeCount = tools.length - readOnlyCount;
535
+ const coreCount = coreTools.length;
536
+ const loadEnvCount = loadEnvTools.length;
537
+ // Group tools by tag for documentation
538
+ const tagGroups = {};
539
+ for (const tool of tools) {
540
+ const tag = tool.tags[0] || "Other";
541
+ if (!tagGroups[tag])
542
+ tagGroups[tag] = { tools: [], isCore: tool.isCore };
543
+ tagGroups[tag].tools.push(tool.name);
544
+ }
545
+ const tagDocs = Object.entries(tagGroups)
546
+ .map(([tag, info]) => ` * ${tag}: ${info.tools.length} tools ${info.isCore ? '(CORE)' : '(loadEnv)'}`)
547
+ .join("\n");
548
+ // Generate env var documentation for loadEnv groups
549
+ const loadEnvDocs = Object.entries(loadEnvGroups)
550
+ .map(([envVar, groupTools]) => ` * ${envVar}=true → ${groupTools.length} tools`)
551
+ .join("\n");
552
+ // Generate the combined tools array
553
+ const loadEnvVarNames = Object.keys(loadEnvGroups).map(ev => `...${ev.toLowerCase().replace(/_/g, '')}Tools`);
554
+ return `/**
555
+ * Auto-generated OpenAPI client tools
556
+ *
557
+ * Source: ${config.url || config.file || "OpenAPI spec"}
558
+ * API: ${spec.info.title} v${spec.info.version}
559
+ * Generated: ${new Date().toISOString()}
560
+ *
561
+ * Tools by category:
562
+ ${tagDocs}
563
+ *
564
+ * Total: ${tools.length} tools (${readOnlyCount} read-only, ${writeCount} write)
565
+ * Core tools: ${coreCount} (always loaded)
566
+ * LoadEnv tools: ${loadEnvCount} (require env var to enable)
567
+ *
568
+ * To enable additional tool groups, set environment variables:
569
+ ${loadEnvDocs}
570
+ *
571
+ * DO NOT EDIT MANUALLY - Regenerate with: pets generate-openapi
572
+ */
573
+
574
+ import { z, type ToolDefinition, createLogger, isReadOnly, filterToolsForReadOnly } from "openpets-sdk"
575
+
576
+ const logger = createLogger("${petName}-client")
577
+
578
+ // Configuration
579
+ const baseUrl = "${baseUrl}"
580
+ const apiKey = process.env.${authEnvVar}
581
+
582
+ // Read-only mode check
583
+ const readOnlyMode = isReadOnly("${petName}")
584
+
585
+ if (readOnlyMode) {
586
+ logger.info("READ-ONLY MODE - write operations disabled")
587
+ }
588
+
589
+ // Common headers
590
+ const headers: Record<string, string> = {
591
+ "Content-Type": "application/json",
592
+ "Accept": "application/json",
593
+ }
594
+ ${authSetup}
595
+
596
+ /**
597
+ * Fetch wrapper with error handling
598
+ */
599
+ async function fetchAPI(url: string, options: RequestInit): Promise<string> {
600
+ try {
601
+ const response = await fetch(url, options)
602
+
603
+ if (!response.ok) {
604
+ const errorText = await response.text()
605
+ return JSON.stringify({
606
+ success: false,
607
+ error: \`HTTP \${response.status}: \${errorText}\`,
608
+ status: response.status
609
+ }, null, 2)
610
+ }
611
+
612
+ const contentType = response.headers.get("content-type")
613
+ if (contentType?.includes("application/json")) {
614
+ const data = await response.json()
615
+ return JSON.stringify(data, null, 2)
616
+ }
617
+
618
+ const text = await response.text()
619
+ return JSON.stringify({ success: true, data: text }, null, 2)
620
+ } catch (error: any) {
621
+ return JSON.stringify({
622
+ success: false,
623
+ error: error.message
624
+ }, null, 2)
625
+ }
626
+ }
627
+
628
+ // Tools marked as write operations (filtered in read-only mode)
629
+ // Based on HTTP method: POST, PUT, PATCH, DELETE are write operations
630
+ const writeToolNames = new Set([
631
+ ${tools.filter(t => !t.isReadOnly).map(t => ` "${t.name}"`).join(",\n")}
632
+ ])
633
+
634
+ // ============================================================================
635
+ // CORE TOOLS - Always loaded (${coreCount} tools)
636
+ // ============================================================================
637
+ const coreTools: ToolDefinition[] = [
638
+ ${coreToolDefinitions}
639
+ ]
640
+
641
+ // ============================================================================
642
+ // LOADENV TOOLS - Require environment variable to enable
643
+ // ============================================================================
644
+ ${loadEnvGroupDefinitions.join("\n")}
645
+
646
+ /**
647
+ * All tools combined (core + enabled loadEnv groups)
648
+ */
649
+ const allTools: ToolDefinition[] = [
650
+ ...coreTools,
651
+ ${loadEnvVarNames.join(",\n ")}
652
+ ]
653
+
654
+ // Log which tool groups are enabled
655
+ const enabledGroups: string[] = ['core']
656
+ ${Object.keys(loadEnvGroups).map(ev => `if (process.env.${ev} === 'true') enabledGroups.push('${ev}')`).join("\n")}
657
+ logger.info(\`Loaded tool groups: \${enabledGroups.join(', ')} (\${allTools.length} tools total)\`)
658
+
659
+ /**
660
+ * Filter tools by read-only mode using HTTP method classification
661
+ */
662
+ function filterByReadOnlyMode(tools: ToolDefinition[]): ToolDefinition[] {
663
+ if (!readOnlyMode) return tools
664
+
665
+ const filtered = tools.filter(t => !writeToolNames.has(t.name))
666
+ const excluded = tools.length - filtered.length
667
+
668
+ if (excluded > 0) {
669
+ logger.info(\`Filtered \${excluded} write tools in read-only mode\`)
670
+ }
671
+
672
+ return filtered
673
+ }
674
+
675
+ export const openAPITools = filterByReadOnlyMode(allTools)
676
+
677
+ export default openAPITools
678
+ `;
679
+ }
680
+ // ============================================================================
681
+ // Main Generator Function
682
+ // ============================================================================
683
+ export async function generateOpenAPITools(options = {}) {
684
+ const cwd = process.cwd();
685
+ const outputFile = options.outputFile || "openapi-client.ts";
686
+ // Load .env files
687
+ loadDotenv({ path: resolve(cwd, ".env") });
688
+ loadDotenv({ path: resolve(cwd, "../.env") });
689
+ loadDotenv({ path: resolve(cwd, "../../.env") });
690
+ // Determine config source
691
+ let config;
692
+ let petName;
693
+ // Check for URL or file from options first
694
+ if (options.url || options.file) {
695
+ const packageJsonPath = resolve(cwd, "package.json");
696
+ if (existsSync(packageJsonPath)) {
697
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
698
+ petName = packageJson.name?.replace(/^@openpets\//, "") || basename(cwd);
699
+ }
700
+ else {
701
+ petName = basename(cwd);
702
+ }
703
+ config = {
704
+ url: options.url,
705
+ file: options.file,
706
+ baseUrl: options.baseUrl,
707
+ };
708
+ }
709
+ else {
710
+ // Load from package.json
711
+ const loaded = loadConfigFromPackageJson(cwd);
712
+ if (!loaded) {
713
+ return {
714
+ success: false,
715
+ message: "No OpenAPI configuration found. Use --url or --file options, or add openapiSpec to package.json"
716
+ };
717
+ }
718
+ config = loaded.config;
719
+ petName = loaded.petName;
720
+ }
721
+ if (!config.url && !config.file) {
722
+ return {
723
+ success: false,
724
+ message: "No OpenAPI spec URL or file specified. Use --url or --file options."
725
+ };
726
+ }
727
+ if (options.verbose) {
728
+ console.log(`Pet Name: ${petName}`);
729
+ console.log(`Source: ${config.url || config.file}`);
730
+ }
731
+ try {
732
+ // Fetch or read the OpenAPI spec
733
+ let spec;
734
+ if (config.url) {
735
+ if (options.verbose) {
736
+ console.log(`Fetching OpenAPI spec from: ${config.url}`);
737
+ }
738
+ spec = await fetchOpenAPISpec(config.url);
739
+ }
740
+ else {
741
+ const filePath = resolve(cwd, config.file);
742
+ if (options.verbose) {
743
+ console.log(`Reading OpenAPI spec from: ${filePath}`);
744
+ }
745
+ spec = readOpenAPISpec(filePath);
746
+ }
747
+ if (options.verbose) {
748
+ console.log(`API: ${spec.info.title} v${spec.info.version}`);
749
+ console.log(`Paths: ${Object.keys(spec.paths).length}`);
750
+ }
751
+ // Dump spec for debugging
752
+ if (options.dumpSpec) {
753
+ const specFile = resolve(cwd, "openapi-spec-dump.json");
754
+ writeFileSync(specFile, JSON.stringify(spec, null, 2));
755
+ console.log(`Dumped spec to: ${specFile}`);
756
+ }
757
+ // Parse spec into tools
758
+ const tools = parseOpenAPISpec(spec, petName, config);
759
+ if (options.verbose) {
760
+ console.log(`Generated ${tools.length} tools`);
761
+ console.log("\nTools:");
762
+ for (const tool of tools) {
763
+ const marker = tool.isReadOnly ? "read" : "write";
764
+ console.log(` [${marker}] ${tool.name} - ${tool.method} ${tool.path}`);
765
+ }
766
+ }
767
+ // Generate client file
768
+ const content = generateOpenAPIClientFile(spec, tools, config, petName);
769
+ if (options.dryRun) {
770
+ console.log("\n--- Generated openapi-client.ts ---");
771
+ console.log(content.slice(0, 5000));
772
+ if (content.length > 5000) {
773
+ console.log(`\n... (${content.length - 5000} more characters)`);
774
+ }
775
+ console.log("--- End ---\n");
776
+ }
777
+ else {
778
+ const outputPath = resolve(cwd, outputFile);
779
+ writeFileSync(outputPath, content);
780
+ }
781
+ const readOnlyCount = tools.filter(t => t.isReadOnly).length;
782
+ const writeCount = tools.length - readOnlyCount;
783
+ return {
784
+ success: true,
785
+ message: options.dryRun
786
+ ? `Would generate ${tools.length} tools (${readOnlyCount} read-only, ${writeCount} write) to ${outputFile}`
787
+ : `Generated ${tools.length} tools (${readOnlyCount} read-only, ${writeCount} write) to ${outputFile}`,
788
+ toolCount: tools.length,
789
+ outputPath: resolve(cwd, outputFile),
790
+ tools,
791
+ };
792
+ }
793
+ catch (error) {
794
+ return {
795
+ success: false,
796
+ message: `Failed to generate OpenAPI tools: ${error.message}`
797
+ };
798
+ }
799
+ }
800
+ //# sourceMappingURL=openapi-generator.js.map