webmcp-codegen 0.0.1 → 0.2.0

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.
@@ -0,0 +1,238 @@
1
+ import {
2
+ dedupeNames
3
+ } from "./chunk-BIKKPCRT.js";
4
+ import {
5
+ pascalCase
6
+ } from "./chunk-5L4KN6F4.js";
7
+
8
+ // src/config.ts
9
+ import { access } from "fs/promises";
10
+ import { join, resolve } from "path";
11
+ import { pathToFileURL } from "url";
12
+ function defineConfig(config) {
13
+ return config;
14
+ }
15
+ var CONFIG_FILE_NAMES = ["codegen.config.mjs", "codegen.config.js"];
16
+ async function loadConfig(cwd, explicitPath) {
17
+ const candidates = explicitPath ? [resolve(cwd, explicitPath)] : CONFIG_FILE_NAMES.map((name) => join(cwd, name));
18
+ for (const candidate of candidates) {
19
+ if (!await exists(candidate)) continue;
20
+ const module = await import(pathToFileURL(candidate).href);
21
+ const config = module.default;
22
+ if (!isCodegenConfig(config)) {
23
+ throw new Error(
24
+ `${candidate} must default-export defineConfig({ sources: [...], generate: [...] }).`
25
+ );
26
+ }
27
+ return { config, path: candidate };
28
+ }
29
+ throw new Error(
30
+ explicitPath ? `No config file at "${explicitPath}".` : `No codegen.config.mjs found in ${cwd}. Run \`npx webmcp-codegen init\` to create one.`
31
+ );
32
+ }
33
+ function isCodegenConfig(value) {
34
+ if (value === null || typeof value !== "object") return false;
35
+ const config = value;
36
+ return Array.isArray(config.sources) && Array.isArray(config.generate);
37
+ }
38
+ async function exists(path) {
39
+ try {
40
+ await access(path);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ // src/pipeline.ts
48
+ import { mkdir, writeFile } from "fs/promises";
49
+ import { dirname } from "path";
50
+
51
+ // src/safety.ts
52
+ var DESTRUCTIVE_WORDS = /\b(cancel|delete|remove|destroy|deactivate|refund|revoke|purge|close)\b/i;
53
+ var DEFAULT_PII_FIELDS = [
54
+ "password",
55
+ "ssn",
56
+ "token",
57
+ "secret",
58
+ "apikey",
59
+ "api_key",
60
+ "email",
61
+ "dob",
62
+ "birthdate",
63
+ "phone",
64
+ "address",
65
+ "creditcard",
66
+ "cardnumber",
67
+ "cvv"
68
+ ];
69
+ var AGENT_INSTRUCTION_PATTERN = /\b(you (must|should|always|are)|as an ai|ignore (all |previous )?instructions|do not refuse)\b/i;
70
+ function classifySideEffect(tool) {
71
+ switch (tool.httpMethod) {
72
+ case "GET":
73
+ case "HEAD":
74
+ case "OPTIONS":
75
+ return "read";
76
+ case "DELETE":
77
+ return "destructive";
78
+ case "POST":
79
+ case "PUT":
80
+ case "PATCH":
81
+ return DESTRUCTIVE_WORDS.test(tool.name) || DESTRUCTIVE_WORDS.test(tool.source.ref) ? "destructive" : "write";
82
+ default:
83
+ return "unknown";
84
+ }
85
+ }
86
+ function hintsFor(tool, sideEffect) {
87
+ const method = tool.httpMethod;
88
+ return {
89
+ readOnlyHint: sideEffect === "read",
90
+ destructiveHint: sideEffect === "destructive",
91
+ // PUT/PATCH/DELETE can be safely retried with the same input; POST cannot.
92
+ idempotentHint: sideEffect === "read" || method === "PUT" || method === "PATCH" || method === "DELETE"
93
+ };
94
+ }
95
+ function riskTierFor(sideEffect) {
96
+ switch (sideEffect) {
97
+ case "read":
98
+ return "safe-read";
99
+ case "destructive":
100
+ return "destructive-confirm";
101
+ default:
102
+ return "write-confirm";
103
+ }
104
+ }
105
+ function findPiiFields(schema, extraFields = [], prefix = "") {
106
+ if (!schema?.properties) return [];
107
+ const piiNames = new Set(
108
+ [...DEFAULT_PII_FIELDS, ...extraFields].map((name) => name.toLowerCase())
109
+ );
110
+ const found = [];
111
+ for (const [key, fieldSchema] of Object.entries(schema.properties)) {
112
+ const path = prefix ? `${prefix}.${key}` : key;
113
+ const normalizedKey = key.toLowerCase().replace(/[-_]/g, "");
114
+ const looksSensitive = piiNames.has(key.toLowerCase()) || piiNames.has(normalizedKey) || [...piiNames].some((name) => normalizedKey === name.replace(/[-_]/g, ""));
115
+ if (looksSensitive) found.push(path);
116
+ found.push(...findPiiFields(fieldSchema, extraFields, path));
117
+ }
118
+ return found;
119
+ }
120
+ function reviewTools(candidates, safety = {}) {
121
+ const excluded = (safety.exclude ?? []).map((pattern) => pattern.toLowerCase());
122
+ return candidates.filter(
123
+ (tool) => !excluded.some(
124
+ (pattern) => tool.name.toLowerCase().includes(pattern) || tool.source.ref.toLowerCase().includes(pattern)
125
+ )
126
+ ).map((tool) => {
127
+ const sideEffect = classifySideEffect(tool);
128
+ return {
129
+ ...tool,
130
+ sideEffect,
131
+ riskTier: riskTierFor(sideEffect),
132
+ hints: hintsFor(tool, sideEffect),
133
+ piiInOutput: findPiiFields(tool.outputSchema, safety.piiFields)
134
+ };
135
+ });
136
+ }
137
+ function auditTools(tools, renames = []) {
138
+ const findings = [];
139
+ for (const rename of renames) {
140
+ findings.push({
141
+ level: "warning",
142
+ tool: rename.to,
143
+ message: `Renamed "${rename.from}" \u2192 "${rename.to}" to keep tool names unique.`
144
+ });
145
+ }
146
+ for (const tool of tools) {
147
+ if (!tool.description || tool.description.trim().length === 0) {
148
+ findings.push({
149
+ level: "error",
150
+ tool: tool.name,
151
+ message: "No description. Agents pick tools by description \u2014 this tool is invisible."
152
+ });
153
+ continue;
154
+ }
155
+ if (tool.descriptionSource === "generated-template") {
156
+ findings.push({
157
+ level: "warning",
158
+ tool: tool.name,
159
+ message: `Description is just "${tool.description}" (no summary in the source). Write one sentence about what it does and why \u2014 it goes straight into the agent's prompt.`
160
+ });
161
+ }
162
+ if (AGENT_INSTRUCTION_PATTERN.test(tool.description)) {
163
+ findings.push({
164
+ level: "warning",
165
+ tool: tool.name,
166
+ message: "The description reads like instructions to the agent, not a description of the tool. Describe what the tool does; never try to steer the agent from here."
167
+ });
168
+ }
169
+ if (tool.riskTier === "safe-read" && DESTRUCTIVE_WORDS.test(tool.name)) {
170
+ findings.push({
171
+ level: "error",
172
+ tool: tool.name,
173
+ message: `The name suggests something destructive but ${tool.httpMethod} is a safe verb. Check the spec \u2014 a GET named like a delete is either mislabeled or a design smell.`
174
+ });
175
+ }
176
+ if (tool.piiInOutput.length > 0) {
177
+ findings.push({
178
+ level: "warning",
179
+ tool: tool.name,
180
+ message: `Response may expose ${tool.piiInOutput.join(", ")}. These fields reach the agent \u2014 exclude them in execute() unless they are truly needed.`
181
+ });
182
+ }
183
+ if (tool.requiresAuth && tool.riskTier !== "safe-read") {
184
+ findings.push({
185
+ level: "warning",
186
+ tool: tool.name,
187
+ message: "This mutating tool wraps an authenticated endpoint. It runs with the page's session \u2014 make sure your server-side authorization checks apply to tool calls too."
188
+ });
189
+ }
190
+ }
191
+ return findings;
192
+ }
193
+
194
+ // src/pipeline.ts
195
+ async function runGenerate(config, options) {
196
+ const candidates = (await Promise.all(config.sources.map((source) => source.collect()))).flat();
197
+ const { names, renames } = dedupeNames(candidates);
198
+ const named = candidates.map((candidate, index) => {
199
+ const name = names[index] ?? candidate.name;
200
+ return { ...candidate, name, inputTypeName: `${pascalCase(name)}Input` };
201
+ });
202
+ const tools = reviewTools(named, config.safety);
203
+ const findings = options.skipAudit ? [] : auditTools(tools, renames);
204
+ const errors = findings.filter((finding) => finding.level === "error");
205
+ const blocked = errors.length > 0 && !options.force && !options.skipAudit;
206
+ if (blocked) {
207
+ return { tools, findings, files: [], blocked, wrote: false };
208
+ }
209
+ const files = [];
210
+ for (const generator of config.generate) {
211
+ files.push(...await generator.generate(tools, options.cwd));
212
+ }
213
+ let wrote = false;
214
+ if (!options.dryRun) {
215
+ for (const file of files) {
216
+ if (file.action === "unchanged" && !file.conflict) continue;
217
+ const target = file.conflict ?? file.path;
218
+ await mkdir(dirname(target), { recursive: true });
219
+ await writeFile(target, file.conflict ? conflictContents(file) : file.contents);
220
+ }
221
+ wrote = true;
222
+ }
223
+ return { tools, findings, files, blocked, wrote };
224
+ }
225
+ function conflictContents(file) {
226
+ return `// webmcp-codegen could not regenerate ${file.path} because its generated
227
+ // region was edited by hand. Review this version, then merge it manually.
228
+
229
+ ` + file.contents;
230
+ }
231
+
232
+ export {
233
+ defineConfig,
234
+ CONFIG_FILE_NAMES,
235
+ loadConfig,
236
+ runGenerate
237
+ };
238
+ //# sourceMappingURL=chunk-R3DEBBQ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/pipeline.ts","../src/safety.ts"],"sourcesContent":["/**\n * Config: `defineConfig` for authoring, `loadConfig` for the CLI.\n *\n * Config files are plain JavaScript (`codegen.config.mjs`) so the CLI can\n * load them with a plain dynamic import — no TypeScript loader, no build\n * step, no extra dependencies. If you want types while authoring, that is\n * what `defineConfig` is for:\n *\n * import { defineConfig } from \"webmcp-codegen\";\n * export default defineConfig({ ... });\n */\n\nimport { access } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { CodegenConfig } from \"./types.js\";\n\n/** Identity function whose only job is type-checking the config object. */\nexport function defineConfig(config: CodegenConfig): CodegenConfig {\n return config;\n}\n\nexport const CONFIG_FILE_NAMES = [\"codegen.config.mjs\", \"codegen.config.js\"];\n\n/**\n * Find and load the config file. Resolving relative to `cwd` keeps the CLI\n * usable from any directory, the same way `eslint -c` behaves.\n */\nexport async function loadConfig(\n cwd: string,\n explicitPath?: string,\n): Promise<{ config: CodegenConfig; path: string }> {\n const candidates = explicitPath\n ? [resolve(cwd, explicitPath)]\n : CONFIG_FILE_NAMES.map((name) => join(cwd, name));\n\n for (const candidate of candidates) {\n if (!(await exists(candidate))) continue;\n const module = (await import(pathToFileURL(candidate).href)) as { default?: unknown };\n const config = module.default;\n if (!isCodegenConfig(config)) {\n throw new Error(\n `${candidate} must default-export defineConfig({ sources: [...], generate: [...] }).`,\n );\n }\n return { config, path: candidate };\n }\n\n throw new Error(\n explicitPath\n ? `No config file at \"${explicitPath}\".`\n : `No codegen.config.mjs found in ${cwd}. Run \\`npx webmcp-codegen init\\` to create one.`,\n );\n}\n\n/** The lightest possible shape check — clear error beats deep validation. */\nfunction isCodegenConfig(value: unknown): value is CodegenConfig {\n if (value === null || typeof value !== \"object\") return false;\n const config = value as Record<string, unknown>;\n return Array.isArray(config.sources) && Array.isArray(config.generate);\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * The pipeline: sources → normalize → safety review → audit → write.\n *\n * This module is the only place the stages meet. It owns no opinions of its\n * own — naming, safety, and file formats all live in their own modules — it\n * just runs them in order and produces one honest report of what happened\n * (or what *would* happen, when called with `write: false`).\n */\n\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { dedupeNames } from \"./naming.js\";\nimport { auditTools, reviewTools } from \"./safety.js\";\nimport { pascalCase } from \"./schema.js\";\nimport type { AuditFinding, CodegenConfig, GeneratedFile, ReviewedTool } from \"./types.js\";\n\nexport interface GenerateOptions {\n /** Project root. Everything (config, spec paths, outDir) resolves from here. */\n cwd: string;\n /** Preview mode: compute everything, write nothing. */\n dryRun?: boolean;\n /** Skip the audit pass entirely (classification still runs — output needs it). */\n skipAudit?: boolean;\n /** Write even when the audit found errors. The report still shows them. */\n force?: boolean;\n}\n\nexport interface GenerateResult {\n tools: ReviewedTool[];\n findings: AuditFinding[];\n files: GeneratedFile[];\n /** True when audit errors stopped any file from being written. */\n blocked: boolean;\n /** True when this run actually wrote files (false for dry runs and blocks). */\n wrote: boolean;\n}\n\nexport async function runGenerate(\n config: CodegenConfig,\n options: GenerateOptions,\n): Promise<GenerateResult> {\n // 1. Collect candidate tools from every configured source.\n const candidates = (await Promise.all(config.sources.map((source) => source.collect()))).flat();\n\n // 2. Normalize: make names unique before anything downstream sees them.\n // The input type name is derived from the *final* name so they never drift.\n const { names, renames } = dedupeNames(candidates);\n const named = candidates.map((candidate, index) => {\n const name = names[index] ?? candidate.name;\n return { ...candidate, name, inputTypeName: `${pascalCase(name)}Input` };\n });\n\n // 3. Safety review: classify side effects, compute hints, scan for PII,\n // apply config exclusions.\n const tools = reviewTools(named, config.safety);\n\n // 4. Audit. Errors block the write unless --force (or --skip-audit) was passed.\n const findings = options.skipAudit ? [] : auditTools(tools, renames);\n const errors = findings.filter((finding) => finding.level === \"error\");\n const blocked = errors.length > 0 && !options.force && !options.skipAudit;\n\n if (blocked) {\n return { tools, findings, files: [], blocked, wrote: false };\n }\n\n // 5. Generate the files, then write them (unless this is a dry run).\n const files: GeneratedFile[] = [];\n for (const generator of config.generate) {\n files.push(...(await generator.generate(tools, options.cwd)));\n }\n\n let wrote = false;\n if (!options.dryRun) {\n for (const file of files) {\n if (file.action === \"unchanged\" && !file.conflict) continue;\n // A conflict means a human edited the generated region by hand:\n // leave their file alone and put our version in a `.new` sibling.\n const target = file.conflict ?? file.path;\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, file.conflict ? conflictContents(file) : file.contents);\n }\n wrote = true;\n }\n\n return { tools, findings, files, blocked, wrote };\n}\n\n/**\n * When a hand-edited file blocks regeneration, the `.new` file explains\n * itself at the top so nobody mistakes it for something to import.\n */\nfunction conflictContents(file: GeneratedFile): string {\n return (\n `// webmcp-codegen could not regenerate ${file.path} because its generated\\n` +\n `// region was edited by hand. Review this version, then merge it manually.\\n\\n` +\n file.contents\n );\n}\n","/**\n * The safety layer.\n *\n * Nothing gets written to disk until every candidate tool has been through\n * here. This layer does three jobs:\n *\n * 1. Classify — what does calling this tool do to the world?\n * (read / write / destructive, from the HTTP verb plus name heuristics)\n * 2. Hint — derive the WebMCP tool hints (readOnlyHint etc.) from that\n * 3. Audit — lint the result and report problems in plain language\n *\n * Every rule is a heuristic with an escape hatch: the generated code carries\n * the classification in plain sight, and the developer owns the final file.\n */\n\nimport type {\n AuditFinding,\n CandidateTool,\n JsonSchema,\n ReviewedTool,\n RiskTier,\n SafetyOptions,\n SideEffect,\n ToolHints,\n} from \"./types.js\";\n\n/**\n * Words that signal \"this changes something the user can't easily undo\",\n * even when the HTTP verb looks innocent. `POST /orders/{id}/cancel` is the\n * classic case: a POST that behaves like a DELETE.\n */\nconst DESTRUCTIVE_WORDS =\n /\\b(cancel|delete|remove|destroy|deactivate|refund|revoke|purge|close)\\b/i;\n\n/**\n * Field names that usually hold personal data or secrets. Matched against\n * the last segment of a field path, case-insensitively. Teams extend this\n * list via `safety.piiFields` in the config.\n */\nconst DEFAULT_PII_FIELDS = [\n \"password\",\n \"ssn\",\n \"token\",\n \"secret\",\n \"apikey\",\n \"api_key\",\n \"email\",\n \"dob\",\n \"birthdate\",\n \"phone\",\n \"address\",\n \"creditcard\",\n \"cardnumber\",\n \"cvv\",\n];\n\n/**\n * Phrases that suggest a description is trying to *instruct the agent*\n * instead of describing the tool — a known prompt-injection smell.\n */\nconst AGENT_INSTRUCTION_PATTERN =\n /\\b(you (must|should|always|are)|as an ai|ignore (all |previous )?instructions|do not refuse)\\b/i;\n\n/** Step 1: classify what calling the tool does. */\nexport function classifySideEffect(tool: CandidateTool): SideEffect {\n switch (tool.httpMethod) {\n case \"GET\":\n case \"HEAD\":\n case \"OPTIONS\":\n // A safe verb whose name says otherwise is suspicious — audit flags it.\n return \"read\";\n case \"DELETE\":\n return \"destructive\";\n case \"POST\":\n case \"PUT\":\n case \"PATCH\":\n // Upgrade nominally-\"write\" verbs when the name says it can't be undone.\n return DESTRUCTIVE_WORDS.test(tool.name) || DESTRUCTIVE_WORDS.test(tool.source.ref)\n ? \"destructive\"\n : \"write\";\n default:\n return \"unknown\";\n }\n}\n\n/** Step 2: derive the WebMCP hints from the classification. */\nexport function hintsFor(tool: CandidateTool, sideEffect: SideEffect): ToolHints {\n const method = tool.httpMethod;\n return {\n readOnlyHint: sideEffect === \"read\",\n destructiveHint: sideEffect === \"destructive\",\n // PUT/PATCH/DELETE can be safely retried with the same input; POST cannot.\n idempotentHint:\n sideEffect === \"read\" || method === \"PUT\" || method === \"PATCH\" || method === \"DELETE\",\n };\n}\n\nexport function riskTierFor(sideEffect: SideEffect): RiskTier {\n switch (sideEffect) {\n case \"read\":\n return \"safe-read\";\n case \"destructive\":\n return \"destructive-confirm\";\n default:\n return \"write-confirm\";\n }\n}\n\n/**\n * Walk a schema and return the paths of fields that look like PII or\n * secrets, e.g. \"user.email\". Only *output* schemas are scanned: the\n * security-relevant direction is data leaving the page and reaching the agent.\n */\nexport function findPiiFields(\n schema: JsonSchema | undefined,\n extraFields: string[] = [],\n prefix = \"\",\n): string[] {\n if (!schema?.properties) return [];\n const piiNames = new Set(\n [...DEFAULT_PII_FIELDS, ...extraFields].map((name) => name.toLowerCase()),\n );\n const found: string[] = [];\n\n for (const [key, fieldSchema] of Object.entries(schema.properties)) {\n const path = prefix ? `${prefix}.${key}` : key;\n const normalizedKey = key.toLowerCase().replace(/[-_]/g, \"\");\n const looksSensitive =\n piiNames.has(key.toLowerCase()) ||\n piiNames.has(normalizedKey) ||\n [...piiNames].some((name) => normalizedKey === name.replace(/[-_]/g, \"\"));\n if (looksSensitive) found.push(path);\n // Recurse into nested objects (\"user\": { \"email\": ... }).\n found.push(...findPiiFields(fieldSchema, extraFields, path));\n }\n return found;\n}\n\n/** Run the full review: classify, hint, PII-scan. Pure — no I/O. */\nexport function reviewTools(\n candidates: CandidateTool[],\n safety: SafetyOptions = {},\n): ReviewedTool[] {\n const excluded = (safety.exclude ?? []).map((pattern) => pattern.toLowerCase());\n\n return candidates\n .filter(\n (tool) =>\n !excluded.some(\n (pattern) =>\n tool.name.toLowerCase().includes(pattern) ||\n tool.source.ref.toLowerCase().includes(pattern),\n ),\n )\n .map((tool) => {\n const sideEffect = classifySideEffect(tool);\n return {\n ...tool,\n sideEffect,\n riskTier: riskTierFor(sideEffect),\n hints: hintsFor(tool, sideEffect),\n piiInOutput: findPiiFields(tool.outputSchema, safety.piiFields),\n };\n });\n}\n\n/**\n * Step 3: audit the reviewed tools and report in plain language.\n * Errors block file writing (unless --force); warnings never do. This is\n * meant to run in CI like `npm audit` — exit codes, not vibes.\n */\nexport function auditTools(\n tools: ReviewedTool[],\n renames: { from: string; to: string }[] = [],\n): AuditFinding[] {\n const findings: AuditFinding[] = [];\n\n for (const rename of renames) {\n findings.push({\n level: \"warning\",\n tool: rename.to,\n message: `Renamed \"${rename.from}\" → \"${rename.to}\" to keep tool names unique.`,\n });\n }\n\n for (const tool of tools) {\n if (!tool.description || tool.description.trim().length === 0) {\n findings.push({\n level: \"error\",\n tool: tool.name,\n message: \"No description. Agents pick tools by description — this tool is invisible.\",\n });\n continue;\n }\n\n if (tool.descriptionSource === \"generated-template\") {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n `Description is just \"${tool.description}\" (no summary in the source). ` +\n \"Write one sentence about what it does and why — it goes straight into the agent's prompt.\",\n });\n }\n\n if (AGENT_INSTRUCTION_PATTERN.test(tool.description)) {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n \"The description reads like instructions to the agent, not a description of the tool. \" +\n \"Describe what the tool does; never try to steer the agent from here.\",\n });\n }\n\n if (tool.riskTier === \"safe-read\" && DESTRUCTIVE_WORDS.test(tool.name)) {\n findings.push({\n level: \"error\",\n tool: tool.name,\n message:\n `The name suggests something destructive but ${tool.httpMethod} is a safe verb. ` +\n \"Check the spec — a GET named like a delete is either mislabeled or a design smell.\",\n });\n }\n\n if (tool.piiInOutput.length > 0) {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n `Response may expose ${tool.piiInOutput.join(\", \")}. ` +\n \"These fields reach the agent — exclude them in execute() unless they are truly needed.\",\n });\n }\n\n if (tool.requiresAuth && tool.riskTier !== \"safe-read\") {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n \"This mutating tool wraps an authenticated endpoint. It runs with the page's session — \" +\n \"make sure your server-side authorization checks apply to tool calls too.\",\n });\n }\n }\n\n return findings;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,140 @@
1
+ import {
2
+ nameFromRoute,
3
+ toToolName
4
+ } from "./chunk-BIKKPCRT.js";
5
+ import {
6
+ deepDeref,
7
+ deref,
8
+ pascalCase
9
+ } from "./chunk-5L4KN6F4.js";
10
+
11
+ // src/sources/openapi.ts
12
+ import { readFile } from "fs/promises";
13
+ import { resolve } from "path";
14
+ import { parse as parseYaml } from "yaml";
15
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
16
+ function openapi(options) {
17
+ return {
18
+ kind: "openapi",
19
+ async collect() {
20
+ const specPath = resolve(process.cwd(), options.spec);
21
+ const spec = await readSpec(specPath);
22
+ return operationsFromSpec(spec);
23
+ }
24
+ };
25
+ }
26
+ async function readSpec(specPath) {
27
+ let text;
28
+ try {
29
+ text = await readFile(specPath, "utf8");
30
+ } catch {
31
+ throw new Error(
32
+ `Could not read the OpenAPI spec at "${specPath}". Check the "spec" path in codegen.config.`
33
+ );
34
+ }
35
+ return parseYaml(text);
36
+ }
37
+ function operationsFromSpec(spec) {
38
+ const root = spec;
39
+ const paths = root.paths ?? {};
40
+ const rootSecurity = Array.isArray(root.security) && root.security.length > 0;
41
+ const candidates = [];
42
+ for (const [path, pathItem] of Object.entries(paths)) {
43
+ const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
44
+ for (const method of HTTP_METHODS) {
45
+ const operation = pathItem[method];
46
+ if (!operation) continue;
47
+ const upperMethod = method.toUpperCase();
48
+ const ref = `${upperMethod} ${path}`;
49
+ const name = typeof operation.operationId === "string" && operation.operationId.length > 0 ? toToolName(operation.operationId) : nameFromRoute(method, path);
50
+ const opSecurity = operation.security;
51
+ const requiresAuth = Array.isArray(opSecurity) ? opSecurity.length > 0 : rootSecurity;
52
+ candidates.push({
53
+ id: ref,
54
+ name,
55
+ source: { kind: "openapi", ref },
56
+ inputSchema: buildInputSchema(operation, sharedParams, spec),
57
+ outputSchema: findOutputSchema(operation, spec),
58
+ inputTypeName: `${pascalCase(name)}Input`,
59
+ httpMethod: upperMethod,
60
+ // The safety layer refines this — the source only reports the verb.
61
+ sideEffect: "unknown",
62
+ requiresAuth,
63
+ description: pickDescription(operation, upperMethod ?? method.toUpperCase(), path),
64
+ descriptionSource: typeof operation.summary === "string" || typeof operation.description === "string" ? "openapi-summary" : "generated-template"
65
+ });
66
+ }
67
+ }
68
+ if (candidates.length === 0) {
69
+ throw new Error('The OpenAPI spec has no operations under "paths". Nothing to generate.');
70
+ }
71
+ return candidates;
72
+ }
73
+ function buildInputSchema(operation, sharedParams, spec) {
74
+ const properties = {};
75
+ const required = /* @__PURE__ */ new Set();
76
+ const parameters = [
77
+ ...sharedParams,
78
+ ...Array.isArray(operation.parameters) ? operation.parameters : []
79
+ ];
80
+ for (const rawParam of parameters) {
81
+ const param = deref(rawParam, spec);
82
+ if (param.in !== "path" && param.in !== "query") continue;
83
+ if (typeof param.name !== "string") continue;
84
+ const fieldSchema = param.schema ? deepDeref(param.schema, spec) : { type: "string" };
85
+ properties[param.name] = typeof param.description === "string" ? { ...fieldSchema, description: param.description } : fieldSchema;
86
+ if (param.in === "path" || param.required === true) required.add(param.name);
87
+ }
88
+ const body = extractJsonBody(operation.requestBody, spec);
89
+ if (body) {
90
+ if (body.schema.type === "object" || body.schema.properties) {
91
+ for (const [key, value] of Object.entries(body.schema.properties ?? {})) {
92
+ properties[key] = value;
93
+ }
94
+ if (body.required) {
95
+ for (const key of body.schema.required ?? []) required.add(key);
96
+ }
97
+ } else {
98
+ properties.body = body.schema;
99
+ if (body.required) required.add("body");
100
+ }
101
+ }
102
+ return { type: "object", properties, required: [...required] };
103
+ }
104
+ function extractJsonBody(requestBody, spec) {
105
+ if (!requestBody || typeof requestBody !== "object") return void 0;
106
+ const body = deref(requestBody, spec);
107
+ const content = body.content;
108
+ const jsonEntry = content?.["application/json"] ?? Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith("+json"))?.[1];
109
+ if (!jsonEntry?.schema) return void 0;
110
+ return {
111
+ schema: deepDeref(jsonEntry.schema, spec),
112
+ required: body.required === true
113
+ };
114
+ }
115
+ function findOutputSchema(operation, spec) {
116
+ const responses = operation.responses;
117
+ if (!responses) return void 0;
118
+ for (const [status, rawResponse] of Object.entries(responses)) {
119
+ if (!status.startsWith("2")) continue;
120
+ const response = deref(rawResponse, spec);
121
+ const content = response.content;
122
+ const jsonEntry = content?.["application/json"] ?? Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith("+json"))?.[1];
123
+ if (jsonEntry?.schema) return deepDeref(jsonEntry.schema, spec);
124
+ }
125
+ return void 0;
126
+ }
127
+ function pickDescription(operation, method, path) {
128
+ if (typeof operation.summary === "string" && operation.summary.trim().length > 0) {
129
+ return operation.summary.trim();
130
+ }
131
+ if (typeof operation.description === "string" && operation.description.trim().length > 0) {
132
+ return operation.description.trim().split("\n")[0];
133
+ }
134
+ return `${method} ${path}`;
135
+ }
136
+
137
+ export {
138
+ openapi
139
+ };
140
+ //# sourceMappingURL=chunk-WYGVTIGI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sources/openapi.ts"],"sourcesContent":["/**\n * The OpenAPI source.\n *\n * Reads an OpenAPI 3.x document (YAML or JSON) and turns every operation\n * into a CandidateTool. This is the highest-reach source: most backend\n * frameworks can already emit an OpenAPI spec, so teams get value without\n * changing any application code.\n *\n * What we read from each operation:\n * - name ← operationId, slugified (falls back to method + path)\n * - description ← summary, else the first line of description, else a template\n * - inputSchema ← path + query parameters merged with the JSON request body\n * - outputSchema ← the first 2xx response's JSON schema, when present\n *\n * Header and cookie parameters are skipped on purpose: agents should not be\n * setting those by hand, and auth headers are the app's job, not the tool's.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { nameFromRoute, toToolName } from \"../naming.js\";\nimport { deepDeref, deref, pascalCase } from \"../schema.js\";\nimport type { CandidateTool, JsonSchema, Source } from \"../types.js\";\n\nexport interface OpenApiSourceOptions {\n /** Path to the OpenAPI document, relative to the project root. */\n spec: string;\n}\n\nconst HTTP_METHODS = [\"get\", \"post\", \"put\", \"patch\", \"delete\", \"head\", \"options\"] as const;\n\n/** Create an OpenAPI source for the config's `sources` array. */\nexport function openapi(options: OpenApiSourceOptions): Source {\n return {\n kind: \"openapi\",\n async collect() {\n const specPath = resolve(process.cwd(), options.spec);\n const spec = await readSpec(specPath);\n return operationsFromSpec(spec);\n },\n };\n}\n\nasync function readSpec(specPath: string): Promise<unknown> {\n let text: string;\n try {\n text = await readFile(specPath, \"utf8\");\n } catch {\n throw new Error(\n `Could not read the OpenAPI spec at \"${specPath}\". Check the \"spec\" path in codegen.config.`,\n );\n }\n // YAML is a superset of JSON, so one parser handles both file types.\n return parseYaml(text);\n}\n\nfunction operationsFromSpec(spec: unknown): CandidateTool[] {\n const root = spec as Record<string, unknown>;\n const paths = (root.paths ?? {}) as Record<string, Record<string, unknown>>;\n const rootSecurity = Array.isArray(root.security) && root.security.length > 0;\n const candidates: CandidateTool[] = [];\n\n for (const [path, pathItem] of Object.entries(paths)) {\n // Parameters declared on the path item apply to every operation under it.\n const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];\n\n for (const method of HTTP_METHODS) {\n const operation = pathItem[method] as Record<string, unknown> | undefined;\n if (!operation) continue;\n\n const upperMethod = method.toUpperCase() as CandidateTool[\"httpMethod\"];\n const ref = `${upperMethod} ${path}`;\n const name =\n typeof operation.operationId === \"string\" && operation.operationId.length > 0\n ? toToolName(operation.operationId)\n : nameFromRoute(method, path);\n\n const opSecurity = operation.security;\n const requiresAuth = Array.isArray(opSecurity) ? opSecurity.length > 0 : rootSecurity;\n\n candidates.push({\n id: ref,\n name,\n source: { kind: \"openapi\", ref },\n inputSchema: buildInputSchema(operation, sharedParams, spec),\n outputSchema: findOutputSchema(operation, spec),\n inputTypeName: `${pascalCase(name)}Input`,\n httpMethod: upperMethod,\n // The safety layer refines this — the source only reports the verb.\n sideEffect: \"unknown\",\n requiresAuth,\n description: pickDescription(operation, upperMethod ?? method.toUpperCase(), path),\n descriptionSource:\n typeof operation.summary === \"string\" || typeof operation.description === \"string\"\n ? \"openapi-summary\"\n : \"generated-template\",\n });\n }\n }\n\n if (candidates.length === 0) {\n throw new Error('The OpenAPI spec has no operations under \"paths\". Nothing to generate.');\n }\n return candidates;\n}\n\n/**\n * Merge path + query parameters and the JSON request body into one object\n * schema — that single object is what the agent fills in when calling the tool.\n */\nfunction buildInputSchema(\n operation: Record<string, unknown>,\n sharedParams: unknown[],\n spec: unknown,\n): JsonSchema {\n const properties: Record<string, JsonSchema> = {};\n const required = new Set<string>();\n\n const parameters = [\n ...sharedParams,\n ...(Array.isArray(operation.parameters) ? operation.parameters : []),\n ];\n\n for (const rawParam of parameters) {\n const param = deref(rawParam as JsonSchema, spec) as Record<string, unknown>;\n // Headers and cookies are transport concerns, not tool inputs.\n if (param.in !== \"path\" && param.in !== \"query\") continue;\n if (typeof param.name !== \"string\") continue;\n\n const fieldSchema = param.schema\n ? deepDeref(param.schema as JsonSchema, spec)\n : { type: \"string\" };\n properties[param.name] =\n typeof param.description === \"string\"\n ? { ...fieldSchema, description: param.description }\n : fieldSchema;\n // Path params are always required by definition; query params say so.\n if (param.in === \"path\" || param.required === true) required.add(param.name);\n }\n\n const body = extractJsonBody(operation.requestBody, spec);\n if (body) {\n if (body.schema.type === \"object\" || body.schema.properties) {\n // The common case: an object body flattens into the tool input.\n for (const [key, value] of Object.entries(body.schema.properties ?? {})) {\n properties[key] = value;\n }\n if (body.required) {\n for (const key of body.schema.required ?? []) required.add(key);\n }\n } else {\n // A non-object body (array, raw string, …) goes under a \"body\" field.\n properties.body = body.schema;\n if (body.required) required.add(\"body\");\n }\n }\n\n return { type: \"object\", properties, required: [...required] };\n}\n\n/** Find the operation's JSON request body schema, if it declares one. */\nfunction extractJsonBody(\n requestBody: unknown,\n spec: unknown,\n): { schema: JsonSchema; required: boolean } | undefined {\n if (!requestBody || typeof requestBody !== \"object\") return undefined;\n const body = deref(requestBody as JsonSchema, spec) as Record<string, unknown>;\n const content = body.content as Record<string, { schema?: JsonSchema }> | undefined;\n // Prefer application/json; accept any \"+json\" media type as a fallback.\n const jsonEntry =\n content?.[\"application/json\"] ??\n Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith(\"+json\"))?.[1];\n if (!jsonEntry?.schema) return undefined;\n return {\n schema: deepDeref(jsonEntry.schema, spec),\n required: body.required === true,\n };\n}\n\n/** The response contract, used by the safety layer's PII scan (and later, docs). */\nfunction findOutputSchema(\n operation: Record<string, unknown>,\n spec: unknown,\n): JsonSchema | undefined {\n const responses = operation.responses as Record<string, unknown> | undefined;\n if (!responses) return undefined;\n // The first 2xx response with a JSON schema wins.\n for (const [status, rawResponse] of Object.entries(responses)) {\n if (!status.startsWith(\"2\")) continue;\n const response = deref(rawResponse as JsonSchema, spec) as Record<string, unknown>;\n const content = response.content as Record<string, { schema?: JsonSchema }> | undefined;\n const jsonEntry =\n content?.[\"application/json\"] ??\n Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith(\"+json\"))?.[1];\n if (jsonEntry?.schema) return deepDeref(jsonEntry.schema, spec);\n }\n return undefined;\n}\n\n/**\n * The description is part of the agent's prompt, so we take the spec's own\n * words when they exist and only fall back to a plain template. Both paths\n * are marked so the audit report shows which descriptions need human love.\n */\nfunction pickDescription(operation: Record<string, unknown>, method: string, path: string): string {\n if (typeof operation.summary === \"string\" && operation.summary.trim().length > 0) {\n return operation.summary.trim();\n }\n if (typeof operation.description === \"string\" && operation.description.trim().length > 0) {\n // Use the first line only — long prose belongs in docs, not in a prompt.\n return operation.description.trim().split(\"\\n\")[0] as string;\n }\n return `${method} ${path}`;\n}\n"],"mappings":"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","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node