capman 0.4.1 → 0.4.2

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,267 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { logger } from './logger';
4
+ export async function parseOpenAPI(specPathOrUrl) {
5
+ const spec = await loadSpec(specPathOrUrl);
6
+ return convertSpec(spec);
7
+ }
8
+ // ─── Load spec from file or URL ───────────────────────────────────────────────
9
+ async function loadSpec(source) {
10
+ // URL
11
+ if (source.startsWith('http://') || source.startsWith('https://')) {
12
+ logger.info(`Fetching OpenAPI spec from: ${source}`);
13
+ const res = await fetch(source);
14
+ if (!res.ok)
15
+ throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
16
+ const text = await res.text();
17
+ return parseSpecText(text, source);
18
+ }
19
+ // Local file
20
+ const resolved = path.resolve(process.cwd(), source);
21
+ if (!fs.existsSync(resolved)) {
22
+ throw new Error(`Spec file not found: ${resolved}`);
23
+ }
24
+ logger.info(`Reading OpenAPI spec from: ${resolved}`);
25
+ const text = fs.readFileSync(resolved, 'utf-8');
26
+ return parseSpecText(text, source);
27
+ }
28
+ function parseSpecText(text, source) {
29
+ // Try JSON first
30
+ try {
31
+ return JSON.parse(text);
32
+ }
33
+ catch { }
34
+ // Try YAML — only if yaml package available
35
+ try {
36
+ const yaml = require('js-yaml');
37
+ return yaml.load(text);
38
+ }
39
+ catch {
40
+ // js-yaml not installed — try basic YAML detection
41
+ if (source.endsWith('.yaml') || source.endsWith('.yml')) {
42
+ throw new Error('YAML spec detected but js-yaml is not installed.\n' +
43
+ 'Install it: npm install js-yaml\n' +
44
+ 'Or convert your spec to JSON first.');
45
+ }
46
+ }
47
+ throw new Error('Could not parse spec — must be valid JSON or YAML');
48
+ }
49
+ // ─── Convert OpenAPI spec to CapmanConfig ─────────────────────────────────────
50
+ function convertSpec(spec) {
51
+ const warnings = [];
52
+ const capabilities = [];
53
+ let skipped = 0;
54
+ // Determine base URL
55
+ const baseUrl = extractBaseUrl(spec);
56
+ // Detect global security schemes
57
+ const securitySchemes = spec.components?.securitySchemes
58
+ ?? spec.securityDefinitions
59
+ ?? {};
60
+ const hasGlobalAuth = Object.keys(securitySchemes).some(k => {
61
+ const s = securitySchemes[k];
62
+ return s.type === 'http' || s.type === 'apiKey' || s.type === 'oauth2';
63
+ });
64
+ // Convert each path + method
65
+ for (const [urlPath, pathItem] of Object.entries(spec.paths ?? {})) {
66
+ const methods = [];
67
+ if (pathItem.get)
68
+ methods.push(['GET', pathItem.get]);
69
+ if (pathItem.post)
70
+ methods.push(['POST', pathItem.post]);
71
+ if (pathItem.put)
72
+ methods.push(['PUT', pathItem.put]);
73
+ if (pathItem.patch)
74
+ methods.push(['PATCH', pathItem.patch]);
75
+ if (pathItem.delete)
76
+ methods.push(['DELETE', pathItem.delete]);
77
+ for (const [method, op] of methods) {
78
+ const result = convertOperation(urlPath, method, op, hasGlobalAuth, securitySchemes);
79
+ if (!result) {
80
+ skipped++;
81
+ warnings.push(`Skipped ${method} ${urlPath} — no useful info to generate capability`);
82
+ continue;
83
+ }
84
+ // Check for duplicate IDs
85
+ const existing = capabilities.find(c => c.id === result.id);
86
+ if (existing) {
87
+ result.id = `${result.id}_${method.toLowerCase()}`;
88
+ warnings.push(`Duplicate ID resolved: ${result.id}`);
89
+ }
90
+ capabilities.push(result);
91
+ }
92
+ }
93
+ const config = {
94
+ app: sanitizeAppName(spec.info.title),
95
+ baseUrl,
96
+ capabilities,
97
+ };
98
+ return {
99
+ config,
100
+ stats: {
101
+ total: capabilities.length,
102
+ skipped,
103
+ warnings,
104
+ },
105
+ };
106
+ }
107
+ // ─── Convert single operation ─────────────────────────────────────────────────
108
+ function convertOperation(urlPath, method, op, hasGlobalAuth, securitySchemes) {
109
+ // Build capability ID
110
+ const id = op.operationId
111
+ ? toSnakeCase(op.operationId)
112
+ : pathToId(method, urlPath);
113
+ // Name and description
114
+ const name = op.summary ?? toHumanName(id);
115
+ const description = op.description ?? op.summary ?? `${method} ${urlPath}`;
116
+ if (description.length < 5)
117
+ return null;
118
+ // Extract params
119
+ const params = extractParams(op);
120
+ // Determine privacy scope
121
+ const privacyLevel = inferPrivacy(op, hasGlobalAuth, securitySchemes);
122
+ // Build examples from path pattern
123
+ const examples = generateExamples(name, description, params);
124
+ // Build returns from response descriptions
125
+ const returns = inferReturns(op, urlPath);
126
+ return {
127
+ id,
128
+ name,
129
+ description,
130
+ examples,
131
+ params,
132
+ returns,
133
+ resolver: {
134
+ type: 'api',
135
+ endpoints: [{ method, path: urlPath }],
136
+ },
137
+ privacy: { level: privacyLevel },
138
+ };
139
+ }
140
+ // ─── Extract params from operation ───────────────────────────────────────────
141
+ function extractParams(op) {
142
+ const params = [];
143
+ // Path and query params
144
+ for (const p of op.parameters ?? []) {
145
+ if (p.in === 'header' || p.in === 'cookie')
146
+ continue;
147
+ const source = p.in === 'path' ? 'user_query' :
148
+ p.in === 'query' ? 'user_query' :
149
+ 'context';
150
+ params.push({
151
+ name: toSnakeCase(p.name),
152
+ description: p.description ?? toHumanName(p.name),
153
+ required: p.required ?? p.in === 'path',
154
+ source,
155
+ });
156
+ }
157
+ // Request body fields (POST/PUT/PATCH)
158
+ const bodyContent = op.requestBody?.content;
159
+ if (bodyContent) {
160
+ const schema = (bodyContent['application/json']?.schema ??
161
+ bodyContent['*/*']?.schema);
162
+ if (schema?.properties) {
163
+ const required = schema.required ?? [];
164
+ for (const [fieldName, field] of Object.entries(schema.properties)) {
165
+ // Skip if already added as a path param
166
+ if (params.find(p => p.name === toSnakeCase(fieldName)))
167
+ continue;
168
+ params.push({
169
+ name: toSnakeCase(fieldName),
170
+ description: field.description ?? toHumanName(fieldName),
171
+ required: required.includes(fieldName),
172
+ source: 'user_query',
173
+ });
174
+ }
175
+ }
176
+ }
177
+ return params;
178
+ }
179
+ // ─── Infer privacy scope ──────────────────────────────────────────────────────
180
+ function inferPrivacy(op, hasGlobalAuth, securitySchemes) {
181
+ // Explicitly no security on this operation
182
+ if (op.security !== undefined && op.security.length === 0)
183
+ return 'public';
184
+ // Check operation tags for admin hints
185
+ const tags = (op.tags ?? []).map(t => t.toLowerCase());
186
+ if (tags.some(t => t.includes('admin') || t.includes('internal')))
187
+ return 'admin';
188
+ // Check operation ID / summary for admin hints
189
+ const hint = `${op.operationId ?? ''} ${op.summary ?? ''}`.toLowerCase();
190
+ if (hint.includes('admin') || hint.includes('manage') || hint.includes('internal')) {
191
+ return 'admin';
192
+ }
193
+ // If global auth exists or operation has security, it's user_owned
194
+ if (hasGlobalAuth || (op.security && op.security.length > 0)) {
195
+ return 'user_owned';
196
+ }
197
+ return 'public';
198
+ }
199
+ // ─── Generate examples ────────────────────────────────────────────────────────
200
+ function generateExamples(name, description, params) {
201
+ const examples = [];
202
+ // Primary example from name
203
+ examples.push(name);
204
+ // Variation from description (first sentence, truncated)
205
+ const firstSentence = description.split(/[.!?]/)[0].trim();
206
+ if (firstSentence && firstSentence !== name && firstSentence.length < 80) {
207
+ examples.push(firstSentence);
208
+ }
209
+ // Param-based example
210
+ const required = params.filter(p => p.required && p.source === 'user_query');
211
+ if (required.length > 0) {
212
+ const paramNames = required.map(p => p.name.replace(/_/g, ' ')).join(' and ');
213
+ examples.push(`${name} by ${paramNames}`);
214
+ }
215
+ return examples.slice(0, 3);
216
+ }
217
+ // ─── Infer returns ────────────────────────────────────────────────────────────
218
+ function inferReturns(op, urlPath) {
219
+ const segments = urlPath.split('/').filter(Boolean);
220
+ const resource = segments
221
+ .filter(s => !s.startsWith('{'))
222
+ .pop() ?? 'data';
223
+ return [resource.replace(/-/g, '_')];
224
+ }
225
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
226
+ function extractBaseUrl(spec) {
227
+ // OpenAPI 3.x
228
+ if (spec.servers?.length) {
229
+ return spec.servers[0].url.replace(/\/$/, '');
230
+ }
231
+ // Swagger 2.x
232
+ if (spec.host) {
233
+ const scheme = 'https';
234
+ const base = spec.basePath ?? '';
235
+ return `${scheme}://${spec.host}${base}`.replace(/\/$/, '');
236
+ }
237
+ return 'https://api.your-app.com';
238
+ }
239
+ function sanitizeAppName(title) {
240
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
241
+ }
242
+ function toSnakeCase(str) {
243
+ return str
244
+ .replace(/([A-Z])/g, '_$1')
245
+ .replace(/[-\s]+/g, '_')
246
+ .toLowerCase()
247
+ .replace(/^_/, '')
248
+ .replace(/__+/g, '_');
249
+ }
250
+ function toHumanName(id) {
251
+ return id
252
+ .replace(/_/g, ' ')
253
+ .replace(/\b\w/g, c => c.toUpperCase());
254
+ }
255
+ function pathToId(method, urlPath) {
256
+ const segments = urlPath
257
+ .split('/')
258
+ .filter(Boolean)
259
+ .map(s => s.startsWith('{') ? s.slice(1, -1) : s)
260
+ .join('_');
261
+ const prefix = method === 'GET' ? 'get' :
262
+ method === 'POST' ? 'create' :
263
+ method === 'PUT' ? 'update' :
264
+ method === 'PATCH' ? 'update' :
265
+ method === 'DELETE' ? 'delete' : 'call';
266
+ return toSnakeCase(`${prefix}_${segments}`);
267
+ }
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/version.js — do not edit manually
2
- export const VERSION = '0.4.1';
2
+ export const VERSION = '0.4.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capman",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Capability Manifest Engine — let AI agents interact with your app without navigating the UI",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",