api-to-cli 0.1.1 → 0.1.3

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 (64) hide show
  1. package/README.md +153 -9
  2. package/examples/openapi/sample-openapi-agent/README.md +12 -0
  3. package/examples/openapi/sample-openapi-agent/agentbridge.manifest.json +85 -0
  4. package/examples/openapi/sample-openapi-agent/cli/README.md +18 -0
  5. package/examples/openapi/sample-openapi-agent/cli/bin/sample-crm-api.js +64 -0
  6. package/examples/openapi/sample-openapi-agent/cli/commands/create-contact.js +59 -0
  7. package/examples/openapi/sample-openapi-agent/cli/commands/delete-contacts-by-contactid.js +45 -0
  8. package/examples/openapi/sample-openapi-agent/cli/commands/get-contacts-by-contactid.js +45 -0
  9. package/examples/openapi/sample-openapi-agent/cli/commands/list-contacts.js +45 -0
  10. package/examples/openapi/sample-openapi-agent/cli/commands/patch-contacts-by-contactid.js +60 -0
  11. package/examples/openapi/sample-openapi-agent/cli/lib/client.js +244 -0
  12. package/examples/openapi/sample-openapi-agent/cli/lib/output.js +21 -0
  13. package/examples/openapi/sample-openapi-agent/cli/package.json +16 -0
  14. package/examples/openapi/sample-openapi-agent/skill/SKILL.md +50 -0
  15. package/examples/openapi/sample-openapi-cli/README.md +18 -0
  16. package/examples/openapi/sample-openapi-cli/bin/sample-crm-api.js +64 -0
  17. package/examples/openapi/sample-openapi-cli/commands/create-contact.js +59 -0
  18. package/examples/openapi/sample-openapi-cli/commands/delete-contacts-by-contactid.js +45 -0
  19. package/examples/openapi/sample-openapi-cli/commands/get-contacts-by-contactid.js +45 -0
  20. package/examples/openapi/sample-openapi-cli/commands/list-contacts.js +45 -0
  21. package/examples/openapi/sample-openapi-cli/commands/patch-contacts-by-contactid.js +60 -0
  22. package/examples/openapi/sample-openapi-cli/lib/client.js +244 -0
  23. package/examples/openapi/sample-openapi-cli/lib/output.js +21 -0
  24. package/examples/openapi/sample-openapi-cli/node_modules/.package-lock.json +15 -0
  25. package/examples/openapi/sample-openapi-cli/node_modules/commander/LICENSE +22 -0
  26. package/examples/openapi/sample-openapi-cli/node_modules/commander/Readme.md +1157 -0
  27. package/examples/openapi/sample-openapi-cli/node_modules/commander/esm.mjs +16 -0
  28. package/examples/openapi/sample-openapi-cli/node_modules/commander/index.js +24 -0
  29. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/argument.js +149 -0
  30. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/command.js +2509 -0
  31. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/error.js +39 -0
  32. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/help.js +520 -0
  33. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/option.js +330 -0
  34. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  35. package/examples/openapi/sample-openapi-cli/node_modules/commander/package-support.json +16 -0
  36. package/examples/openapi/sample-openapi-cli/node_modules/commander/package.json +84 -0
  37. package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/esm.d.mts +3 -0
  38. package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/index.d.ts +969 -0
  39. package/examples/openapi/sample-openapi-cli/package.json +16 -0
  40. package/examples/openapi/sample-openapi.yaml +67 -0
  41. package/examples/trello/trelloapi-agent/README.md +1 -0
  42. package/examples/trello/trelloapi-agent/agentbridge.manifest.json +1 -1
  43. package/examples/trello/trelloapi-agent/cli/commands/get-board.js +4 -0
  44. package/examples/trello/trelloapi-agent/cli/commands/list-board-lists.js +4 -0
  45. package/examples/trello/trelloapi-agent/cli/commands/list-list-cards.js +4 -0
  46. package/examples/trello/trelloapi-agent/cli/lib/client.js +174 -9
  47. package/examples/trello/trelloapi-cli/commands/get-board.js +4 -0
  48. package/examples/trello/trelloapi-cli/commands/list-board-lists.js +4 -0
  49. package/examples/trello/trelloapi-cli/commands/list-list-cards.js +4 -0
  50. package/examples/trello/trelloapi-cli/lib/client.js +174 -9
  51. package/package.json +9 -5
  52. package/src/commands/doctor.js +234 -0
  53. package/src/commands/generate.js +4 -8
  54. package/src/commands/init.js +154 -0
  55. package/src/commands/scaffold.js +9 -9
  56. package/src/commands/validate.js +6 -10
  57. package/src/index.js +21 -5
  58. package/src/lib/generate-cli.js +208 -15
  59. package/src/lib/generate-skill.js +24 -2
  60. package/src/lib/load-config.js +39 -3
  61. package/src/lib/openapi-to-config.js +314 -0
  62. package/src/lib/resolve-config-input.js +50 -0
  63. package/PROJECT_BRIEF.md +0 -65
  64. package/SPEC.md +0 -99
@@ -0,0 +1,314 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const { validateConfig } = require('./load-config');
5
+ const { toKebab } = require('./config-utils');
6
+
7
+ function isUrl(value) {
8
+ return /^https?:\/\//i.test(String(value));
9
+ }
10
+
11
+ function readLocalFile(specPath) {
12
+ const resolved = path.resolve(process.cwd(), specPath);
13
+ if (!fs.existsSync(resolved)) {
14
+ throw new Error(`Spec file not found: ${resolved}`);
15
+ }
16
+
17
+ return fs.readFileSync(resolved, 'utf8');
18
+ }
19
+
20
+ async function readSpecText(specInput) {
21
+ if (isUrl(specInput)) {
22
+ const response = await fetch(String(specInput));
23
+ if (!response.ok) {
24
+ throw new Error(`Unable to fetch spec: HTTP ${response.status}`);
25
+ }
26
+
27
+ return response.text();
28
+ }
29
+
30
+ return readLocalFile(String(specInput));
31
+ }
32
+
33
+ function parseSpec(text) {
34
+ try {
35
+ return JSON.parse(text);
36
+ } catch (_error) {
37
+ return yaml.load(text);
38
+ }
39
+ }
40
+
41
+ function resolveParameter(spec, parameter) {
42
+ if (!parameter || typeof parameter !== 'object') {
43
+ return null;
44
+ }
45
+
46
+ if (!parameter.$ref) {
47
+ return parameter;
48
+ }
49
+
50
+ const match = String(parameter.$ref).match(/^#\/components\/parameters\/([^/]+)$/);
51
+ if (!match) {
52
+ throw new Error(`Unsupported parameter $ref: ${parameter.$ref}`);
53
+ }
54
+
55
+ const key = match[1];
56
+ const resolved = spec.components && spec.components.parameters && spec.components.parameters[key];
57
+
58
+ if (!resolved) {
59
+ throw new Error(`Unable to resolve parameter ref: ${parameter.$ref}`);
60
+ }
61
+
62
+ return resolved;
63
+ }
64
+
65
+ function resolveSchema(spec, schema) {
66
+ if (!schema || typeof schema !== 'object') {
67
+ return null;
68
+ }
69
+
70
+ if (!schema.$ref) {
71
+ return schema;
72
+ }
73
+
74
+ const match = String(schema.$ref).match(/^#\/components\/schemas\/([^/]+)$/);
75
+ if (!match) {
76
+ return null;
77
+ }
78
+
79
+ const key = match[1];
80
+ return spec.components && spec.components.schemas && spec.components.schemas[key];
81
+ }
82
+
83
+ function resolveRequestBody(spec, requestBody) {
84
+ if (!requestBody || typeof requestBody !== 'object') {
85
+ return null;
86
+ }
87
+
88
+ if (!requestBody.$ref) {
89
+ return requestBody;
90
+ }
91
+
92
+ const match = String(requestBody.$ref).match(/^#\/components\/requestBodies\/([^/]+)$/);
93
+ if (!match) {
94
+ throw new Error(`Unsupported requestBody $ref: ${requestBody.$ref}`);
95
+ }
96
+
97
+ const key = match[1];
98
+ const resolved = spec.components && spec.components.requestBodies && spec.components.requestBodies[key];
99
+
100
+ if (!resolved) {
101
+ throw new Error(`Unable to resolve requestBody ref: ${requestBody.$ref}`);
102
+ }
103
+
104
+ return resolved;
105
+ }
106
+
107
+ function dedupeParameters(parameters) {
108
+ const map = new Map();
109
+
110
+ parameters.forEach((parameter) => {
111
+ if (!parameter || !parameter.name) {
112
+ return;
113
+ }
114
+
115
+ const key = `${parameter.in}:${parameter.name}`;
116
+ map.set(key, parameter);
117
+ });
118
+
119
+ return [...map.values()];
120
+ }
121
+
122
+ function inferTypeFromSchema(schema) {
123
+ const resolved = schema && typeof schema === 'object' ? schema : {};
124
+ if (resolved.type === 'integer' || resolved.type === 'number') {
125
+ return 'number';
126
+ }
127
+
128
+ if (resolved.type === 'boolean') {
129
+ return 'boolean';
130
+ }
131
+
132
+ return 'string';
133
+ }
134
+
135
+ function inferType(parameter) {
136
+ return inferTypeFromSchema(parameter.schema || {});
137
+ }
138
+
139
+ function buildCommandName(method, routePath, operation) {
140
+ if (operation.operationId && String(operation.operationId).trim()) {
141
+ return toKebab(String(operation.operationId).trim());
142
+ }
143
+
144
+ const parts = routePath
145
+ .split('/')
146
+ .filter(Boolean)
147
+ .map((part) => {
148
+ if (part.startsWith('{') && part.endsWith('}')) {
149
+ return `by-${part.slice(1, -1)}`;
150
+ }
151
+
152
+ return part;
153
+ });
154
+
155
+ return [method.toLowerCase(), ...parts]
156
+ .join('-')
157
+ .replace(/[^a-zA-Z0-9-]+/g, '-')
158
+ .replace(/-+/g, '-')
159
+ .replace(/^-+|-+$/g, '')
160
+ .toLowerCase();
161
+ }
162
+
163
+ function extractRequestBody(spec, requestBody) {
164
+ const resolved = resolveRequestBody(spec, requestBody);
165
+ if (!resolved) {
166
+ return undefined;
167
+ }
168
+
169
+ const content = resolved.content || {};
170
+ const jsonContent = content['application/json'];
171
+ if (!jsonContent || !jsonContent.schema) {
172
+ return undefined;
173
+ }
174
+
175
+ const schema = resolveSchema(spec, jsonContent.schema) || jsonContent.schema;
176
+ const request = {
177
+ required: Boolean(resolved.required)
178
+ };
179
+
180
+ if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object') {
181
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
182
+ const properties = {};
183
+
184
+ Object.entries(schema.properties).forEach(([name, propSchema]) => {
185
+ const resolvedProp = resolveSchema(spec, propSchema) || propSchema;
186
+ properties[name] = {
187
+ type: inferTypeFromSchema(resolvedProp),
188
+ required: required.has(name),
189
+ description: (resolvedProp && resolvedProp.description) || `${name} field`
190
+ };
191
+ });
192
+
193
+ request.properties = properties;
194
+ }
195
+
196
+ return request;
197
+ }
198
+
199
+ function operationToCommand(spec, method, routePath, operation) {
200
+ const combinedParameters = dedupeParameters([
201
+ ...((operation.__pathParameters || []).map((parameter) => resolveParameter(spec, parameter))),
202
+ ...((operation.parameters || []).map((parameter) => resolveParameter(spec, parameter)))
203
+ ]);
204
+
205
+ const params = {};
206
+
207
+ combinedParameters.forEach((parameter) => {
208
+ if (!parameter || !parameter.name) {
209
+ return;
210
+ }
211
+
212
+ if (parameter.in !== 'path' && parameter.in !== 'query') {
213
+ return;
214
+ }
215
+
216
+ params[parameter.name] = {
217
+ type: inferType(parameter),
218
+ required: parameter.in === 'path' ? true : Boolean(parameter.required),
219
+ description: parameter.description || `${parameter.name} (${parameter.in})`
220
+ };
221
+ });
222
+
223
+ return {
224
+ name: buildCommandName(method, routePath, operation),
225
+ description: operation.summary || operation.description || `${method.toUpperCase()} ${routePath}`,
226
+ method: method.toUpperCase(),
227
+ path: routePath,
228
+ params,
229
+ requestBody: extractRequestBody(spec, operation.requestBody)
230
+ };
231
+ }
232
+
233
+ function normalizeName(rawName) {
234
+ return String(rawName)
235
+ .trim()
236
+ .replace(/[^a-zA-Z0-9]+/g, '-')
237
+ .replace(/^-+|-+$/g, '')
238
+ .toLowerCase();
239
+ }
240
+
241
+ function toConfigFromSpec(spec, overrides = {}) {
242
+ if (!spec || typeof spec !== 'object') {
243
+ throw new Error('Invalid OpenAPI: spec must be an object');
244
+ }
245
+
246
+ const title = (spec.info && spec.info.title) || overrides.name;
247
+ const version = overrides.version || (spec.info && spec.info.version) || '1.0.0';
248
+ const server = (spec.servers && spec.servers[0] && spec.servers[0].url) || '';
249
+
250
+ if (!title) {
251
+ throw new Error('Missing CLI name: provide --name or include info.title in spec');
252
+ }
253
+
254
+ if (!server || !/^https?:\/\//.test(server)) {
255
+ throw new Error('OpenAPI spec must include servers[0].url with http(s) base URL');
256
+ }
257
+
258
+ const paths = spec.paths && typeof spec.paths === 'object' ? spec.paths : {};
259
+ const commands = [];
260
+ const methods = ['get', 'post', 'put', 'patch', 'delete'];
261
+
262
+ Object.entries(paths).forEach(([routePath, pathItem]) => {
263
+ if (!pathItem || typeof pathItem !== 'object') {
264
+ return;
265
+ }
266
+
267
+ methods.forEach((method) => {
268
+ if (!pathItem[method] || typeof pathItem[method] !== 'object') {
269
+ return;
270
+ }
271
+
272
+ const operation = {
273
+ ...pathItem[method],
274
+ __pathParameters: Array.isArray(pathItem.parameters) ? pathItem.parameters : []
275
+ };
276
+
277
+ commands.push(operationToCommand(spec, method, routePath, operation));
278
+ });
279
+ });
280
+
281
+ if (commands.length === 0) {
282
+ throw new Error('No supported operations found in OpenAPI spec');
283
+ }
284
+
285
+ const config = {
286
+ name: normalizeName(title),
287
+ version: String(version),
288
+ apiBase: String(server).replace(/\/$/, ''),
289
+ commands
290
+ };
291
+
292
+ validateConfig(config);
293
+
294
+ return config;
295
+ }
296
+
297
+ async function loadConfigFromOpenApi({ specInput, name, version }) {
298
+ if (!specInput) {
299
+ throw new Error('Missing required flag: --spec <path-or-url>');
300
+ }
301
+
302
+ const text = await readSpecText(specInput);
303
+ const spec = parseSpec(text);
304
+
305
+ if (!spec || typeof spec !== 'object') {
306
+ throw new Error('Failed to parse OpenAPI spec');
307
+ }
308
+
309
+ return toConfigFromSpec(spec, { name, version });
310
+ }
311
+
312
+ module.exports = {
313
+ loadConfigFromOpenApi
314
+ };
@@ -0,0 +1,50 @@
1
+ const path = require('path');
2
+ const { loadConfig } = require('./load-config');
3
+ const { loadConfigFromOpenApi } = require('./openapi-to-config');
4
+
5
+ function hasFlag(flags, key) {
6
+ return Object.prototype.hasOwnProperty.call(flags, key);
7
+ }
8
+
9
+ async function resolveConfigInput(flags) {
10
+ const hasConfig = hasFlag(flags, 'config');
11
+ const hasSpec = hasFlag(flags, 'spec');
12
+
13
+ if (!hasConfig && !hasSpec) {
14
+ throw new Error('Provide one input source: --config <path> or --spec <path-or-url>');
15
+ }
16
+
17
+ if (hasConfig && hasSpec) {
18
+ throw new Error('Use either --config or --spec, not both');
19
+ }
20
+
21
+ if (hasConfig) {
22
+ const configPath = path.resolve(process.cwd(), String(flags.config));
23
+ const config = loadConfig(configPath);
24
+ return {
25
+ config,
26
+ source: {
27
+ type: 'config',
28
+ value: configPath
29
+ }
30
+ };
31
+ }
32
+
33
+ const config = await loadConfigFromOpenApi({
34
+ specInput: String(flags.spec),
35
+ name: flags.name ? String(flags.name) : undefined,
36
+ version: flags.version ? String(flags.version) : undefined
37
+ });
38
+
39
+ return {
40
+ config,
41
+ source: {
42
+ type: 'spec',
43
+ value: String(flags.spec)
44
+ }
45
+ };
46
+ }
47
+
48
+ module.exports = {
49
+ resolveConfigInput
50
+ };
package/PROJECT_BRIEF.md DELETED
@@ -1,65 +0,0 @@
1
- # API-to-CLI Project Brief
2
-
3
- ## Working Name
4
- - Recommended: `AgentBridge`
5
- - NPM package (generator): `api-to-cli`
6
- - Why this split: product name can be brandable, while npm name stays literal and searchable.
7
-
8
- Other viable names:
9
- - `APIBridge`
10
- - `CLIForge`
11
- - `Toolwire`
12
-
13
- ## Problem
14
- Most APIs are not directly usable by AI agents. We want a generator that turns a REST API definition into:
15
- 1. an AI-agent-friendly CLI (JSON-first output), and
16
- 2. an MCP server exposing equivalent tools.
17
-
18
- ## What We Are Building (MVP)
19
- - Input:
20
- - `api-to-cli.config.js` custom config (first)
21
- - OpenAPI support later
22
- - Output:
23
- - Installable Node CLI
24
- - MCP server
25
- - Core behavior:
26
- - JSON output by default
27
- - Consistent JSON error envelope
28
- - Shared auth handling (API key + bearer first)
29
- - Safe-by-default confirmations for destructive methods
30
-
31
- ## Explicit Non-Goals (MVP)
32
- - Full OAuth/device flow in v1
33
- - Every OpenAPI edge case in v1
34
- - Multi-language generators in v1
35
-
36
- ## Demo API Choice (Free + Popular, No First-Party CLI)
37
- Recommended demo target: **Trello REST API**
38
- - Popular real product used by millions
39
- - Free plan available
40
- - Official REST API docs and auth flow
41
- - No first-party Trello CLI from Atlassian (there are community CLIs, which is fine and reinforces the need)
42
-
43
- Key references:
44
- - API reference landing: https://developer.atlassian.com/cloud/trello/rest/
45
- - API introduction (first calls, key + token): https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/
46
- - Authorization details: https://developer.atlassian.com/cloud/trello/guides/rest-api/authorization/
47
- - Pricing (Free plan): https://trello.com/pricing
48
-
49
- ## MVP Command Set for Trello Demo
50
- - `trelloapi me`
51
- - `trelloapi list-boards`
52
- - `trelloapi list-cards --board-id <id>`
53
- - `trelloapi create-card --list-id <id> --name <title> --yes`
54
-
55
- ## Build Sequence
56
- 1. Generator from custom config -> CLI output (GET-only)
57
- 2. Add POST/PUT/PATCH/DELETE with `--yes` guard
58
- 3. Generate MCP server from same config
59
- 4. Add OpenAPI parsing layer
60
-
61
- ## Immediate Next Step (Before Coding)
62
- Finalize:
63
- 1. Product name (`AgentBridge` vs alternatives)
64
- 2. Package names (`api-to-cli`, `@randyventures/api-to-cli`, or other)
65
- 3. Trello as first official example API
package/SPEC.md DELETED
@@ -1,99 +0,0 @@
1
- # AgentBridge MVP Spec
2
-
3
- ## Location
4
- - Project folder: repository root (where this project is cloned)
5
- - This project is standalone and not nested inside another repo
6
-
7
- ## Product Identity
8
- - Product name: `AgentBridge`
9
- - Generator package name: `api-to-cli`
10
-
11
- ## Goal
12
- Given a config file describing API endpoints, generate:
13
- 1. a JSON-first CLI, and
14
- 2. an MCP server exposing matching tools.
15
-
16
- ## Demo API (First Example)
17
- - Trello REST API
18
- - Why: popular, free tier, official docs, no first-party Trello CLI from Atlassian
19
-
20
- ## MVP Scope (Current)
21
- - Input:
22
- - `api-to-cli.config.js`
23
- - Generator commands:
24
- - `validate`
25
- - `generate`
26
- - Output:
27
- - generated CLI project
28
- - Endpoint support:
29
- - GET only
30
- - Auth support:
31
- - env-var credentials injected into header or query
32
-
33
- ## CLI Behavior Requirements
34
- - Every command prints valid JSON to stdout on success.
35
- - Every failure prints JSON in this shape:
36
- ```json
37
- {
38
- "error": true,
39
- "code": "REQUEST_FAILED",
40
- "message": "...",
41
- "details": {}
42
- }
43
- ```
44
- - Exit code:
45
- - `0` on success
46
- - non-zero on error
47
-
48
- ## Security Requirements
49
- - Credentials must come from environment variables only.
50
- - Generated code must not write credentials to files.
51
- - Error output must not include auth headers, tokens, or full request URLs.
52
-
53
- ## Generated Project Structure (MVP)
54
- ```text
55
- <name>-cli/
56
- package.json
57
- bin/<name>
58
- commands/*.js
59
- lib/client.js
60
- lib/output.js
61
- ```
62
-
63
- ## Config Shape (MVP)
64
- ```js
65
- module.exports = {
66
- name: "trelloapi",
67
- version: "1.0.0",
68
- apiBase: "https://api.trello.com/1",
69
- auth: {
70
- credentials: [
71
- { envVar: "TRELLO_KEY", in: "query", name: "key" },
72
- { envVar: "TRELLO_TOKEN", in: "query", name: "token" }
73
- ]
74
- },
75
- commands: [
76
- {
77
- name: "get-board",
78
- description: "Get a board by ID",
79
- method: "GET",
80
- path: "/boards/{boardId}",
81
- params: {
82
- boardId: { type: "string", required: true }
83
- }
84
- }
85
- ]
86
- };
87
- ```
88
-
89
- ## Out of Scope for MVP
90
- - OpenAPI parsing
91
- - MCP generation
92
- - POST/PUT/PATCH/DELETE
93
- - confirmation prompts (`--yes`)
94
- - OAuth flow
95
-
96
- ## Success Criteria
97
- - `api-to-cli validate --config <file>` validates and returns summary JSON.
98
- - `api-to-cli generate --config <file> --output <dir>` creates runnable CLI.
99
- - Generated CLI executes GET commands and emits valid JSON success/error output.