@spec2tools/core 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @spec2tools/core
2
+
3
+ Core utilities for OpenAPI parsing and authentication.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @spec2tools/core
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### OpenAPI Parsing
14
+
15
+ ```ts
16
+ import {
17
+ loadOpenAPISpec,
18
+ extractBaseUrl,
19
+ extractAuthConfig,
20
+ parseOperations,
21
+ formatToolSchema,
22
+ formatToolSignature,
23
+ } from '@spec2tools/core';
24
+
25
+ // Load an OpenAPI specification from file or URL
26
+ const spec = await loadOpenAPISpec('./openapi.yaml');
27
+
28
+ // Extract the base URL
29
+ const baseUrl = extractBaseUrl(spec);
30
+
31
+ // Extract authentication configuration
32
+ const authConfig = extractAuthConfig(spec);
33
+
34
+ // Parse operations into tool definitions
35
+ const tools = parseOperations(spec);
36
+ ```
37
+
38
+ ### Authentication Manager
39
+
40
+ ```ts
41
+ import { AuthManager } from '@spec2tools/core';
42
+
43
+ const authManager = new AuthManager(authConfig);
44
+
45
+ // Check if auth is required
46
+ if (authManager.requiresAuth()) {
47
+ // Perform authentication (OAuth2, API Key, or Bearer token)
48
+ await authManager.authenticate();
49
+ }
50
+
51
+ // Get auth headers for requests
52
+ const headers = authManager.getAuthHeaders();
53
+ ```
54
+
55
+ ### Tool Execution
56
+
57
+ ```ts
58
+ import { createExecutableTools, executeToolByName } from '@spec2tools/core';
59
+
60
+ // Create executable tools from tool definitions
61
+ const tools = createExecutableTools(toolDefs, baseUrl, authManager);
62
+
63
+ // Execute a tool by name
64
+ const result = await executeToolByName(tools, 'getUser', { id: '123' });
65
+ ```
66
+
67
+ ### Error Classes
68
+
69
+ ```ts
70
+ import {
71
+ UnsupportedSchemaError,
72
+ AuthenticationError,
73
+ ToolExecutionError,
74
+ SpecLoadError,
75
+ } from '@spec2tools/core';
76
+ ```
77
+
78
+ ### Types
79
+
80
+ ```ts
81
+ import type {
82
+ HttpMethod,
83
+ Tool,
84
+ AuthType,
85
+ AuthConfig,
86
+ Session,
87
+ OpenAPISpec,
88
+ PathItem,
89
+ Operation,
90
+ Parameter,
91
+ RequestBody,
92
+ MediaType,
93
+ Response,
94
+ SchemaObject,
95
+ SecurityScheme,
96
+ } from '@spec2tools/core';
97
+ ```
98
+
99
+ ## Supported OpenAPI Features
100
+
101
+ ### Supported
102
+ - `GET`, `POST`, `PUT`, `PATCH`, `DELETE` operations
103
+ - Path parameters (string, number, boolean)
104
+ - Query parameters (string, number, boolean)
105
+ - Request body with simple JSON schemas (primitives, flat objects)
106
+ - Security schemes: OAuth2 (authorization code with PKCE), API Key, Bearer token
107
+
108
+ ### Not Supported (throws error)
109
+ - Nested objects beyond 1 level
110
+ - Arrays of objects
111
+ - `anyOf`, `oneOf`, `allOf` schemas
112
+ - File uploads
113
+ - `$ref` references
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,38 @@
1
+ import { Tool } from './types.js';
2
+ interface AgentConfig {
3
+ tools: Tool[];
4
+ model?: string;
5
+ maxSteps?: number;
6
+ }
7
+ /**
8
+ * AI Agent that uses OpenAPI tools
9
+ */
10
+ export declare class Agent {
11
+ private tools;
12
+ private model;
13
+ private maxSteps;
14
+ private conversationHistory;
15
+ constructor(config: AgentConfig);
16
+ /**
17
+ * Get available tools description for the agent
18
+ */
19
+ getToolsDescription(): string;
20
+ /**
21
+ * Process a user message and return the response
22
+ */
23
+ chat(userMessage: string): Promise<string>;
24
+ /**
25
+ * Clear conversation history
26
+ */
27
+ clearHistory(): void;
28
+ /**
29
+ * Get the list of tool names
30
+ */
31
+ getToolNames(): string[];
32
+ /**
33
+ * Get a specific tool by name
34
+ */
35
+ getTool(name: string): Tool | undefined;
36
+ }
37
+ export {};
38
+ //# sourceMappingURL=agent.d.ts.map
package/dist/agent.js ADDED
@@ -0,0 +1,126 @@
1
+ import { generateText, tool, stepCountIs } from 'ai';
2
+ import { openai } from '@ai-sdk/openai';
3
+ import { ToolExecutionError } from './errors.js';
4
+ import chalk from 'chalk';
5
+ const MAX_OUTPUT_LENGTH = 500;
6
+ /**
7
+ * Trim a string if it exceeds the maximum length
8
+ */
9
+ function trimOutput(value) {
10
+ const str = typeof value === 'string' ? value : JSON.stringify(value);
11
+ if (str.length > MAX_OUTPUT_LENGTH) {
12
+ return str.substring(0, MAX_OUTPUT_LENGTH) + '...';
13
+ }
14
+ return str;
15
+ }
16
+ /**
17
+ * AI Agent that uses OpenAPI tools
18
+ */
19
+ export class Agent {
20
+ tools;
21
+ model;
22
+ maxSteps;
23
+ conversationHistory;
24
+ constructor(config) {
25
+ this.tools = config.tools;
26
+ this.model = config.model || 'gpt-5.1-chat-latest';
27
+ this.maxSteps = config.maxSteps || 10;
28
+ this.conversationHistory = [];
29
+ }
30
+ /**
31
+ * Get available tools description for the agent
32
+ */
33
+ getToolsDescription() {
34
+ if (this.tools.length === 0) {
35
+ return 'No tools available.';
36
+ }
37
+ const toolDescriptions = this.tools.map((tool) => {
38
+ return `- ${tool.name}: ${tool.description}`;
39
+ });
40
+ return `I have access to the following tools:\n${toolDescriptions.join('\n')}`;
41
+ }
42
+ /**
43
+ * Process a user message and return the response
44
+ */
45
+ async chat(userMessage) {
46
+ // Add user message to history
47
+ this.conversationHistory.push({
48
+ role: 'user',
49
+ content: userMessage,
50
+ });
51
+ try {
52
+ // Build AI SDK tools from our tool definitions
53
+ const aiTools = {};
54
+ for (const t of this.tools) {
55
+ const toolExecute = t.execute;
56
+ const toolName = t.name;
57
+ aiTools[t.name] = tool({
58
+ description: t.description,
59
+ inputSchema: t.parameters,
60
+ execute: async (params) => {
61
+ console.log(chalk.dim(`\n[Calling ${toolName} with ${JSON.stringify(params)}]`));
62
+ try {
63
+ const result = await toolExecute(params);
64
+ console.log(chalk.dim(`[${toolName} returned: ${trimOutput(result)}]\n`));
65
+ return result;
66
+ }
67
+ catch (error) {
68
+ if (error instanceof ToolExecutionError) {
69
+ console.log(chalk.red(`[${toolName} failed: ${error.message}]\n`));
70
+ throw error;
71
+ }
72
+ throw error;
73
+ }
74
+ },
75
+ });
76
+ }
77
+ // Build system prompt
78
+ const systemPrompt = `You are a helpful AI assistant with access to various API tools.
79
+ When the user asks you to perform actions, use the available tools to help them.
80
+ Always explain what you're doing and present results in a clear, readable format.
81
+ If a tool call fails, explain the error to the user.`;
82
+ // Generate response with tool use
83
+ const result = await generateText({
84
+ model: openai(this.model),
85
+ system: systemPrompt,
86
+ messages: this.conversationHistory,
87
+ tools: aiTools,
88
+ stopWhen: stepCountIs(this.maxSteps),
89
+ });
90
+ // Add assistant response to history
91
+ this.conversationHistory.push({
92
+ role: 'assistant',
93
+ content: result.text,
94
+ });
95
+ return result.text;
96
+ }
97
+ catch (error) {
98
+ const errorMessage = error instanceof Error ? error.message : String(error);
99
+ // Add error response to history
100
+ this.conversationHistory.push({
101
+ role: 'assistant',
102
+ content: `I encountered an error: ${errorMessage}`,
103
+ });
104
+ throw error;
105
+ }
106
+ }
107
+ /**
108
+ * Clear conversation history
109
+ */
110
+ clearHistory() {
111
+ this.conversationHistory = [];
112
+ }
113
+ /**
114
+ * Get the list of tool names
115
+ */
116
+ getToolNames() {
117
+ return this.tools.map((t) => t.name);
118
+ }
119
+ /**
120
+ * Get a specific tool by name
121
+ */
122
+ getTool(name) {
123
+ return this.tools.find((t) => t.name === name);
124
+ }
125
+ }
126
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1,67 @@
1
+ import { AuthConfig } from './types.js';
2
+ export declare class AuthManager {
3
+ private authConfig;
4
+ private accessToken?;
5
+ private refreshToken?;
6
+ private clientId?;
7
+ private clientSecret?;
8
+ private codeVerifier?;
9
+ constructor(authConfig: AuthConfig);
10
+ /**
11
+ * Check if authentication is required
12
+ */
13
+ requiresAuth(): boolean;
14
+ /**
15
+ * Get the current access token
16
+ */
17
+ getAccessToken(): string | undefined;
18
+ /**
19
+ * Get authorization headers for requests
20
+ */
21
+ getAuthHeaders(): Record<string, string>;
22
+ /**
23
+ * Get query parameters for API key auth
24
+ */
25
+ getAuthQueryParams(): Record<string, string>;
26
+ /**
27
+ * Perform authentication based on config type
28
+ */
29
+ authenticate(): Promise<void>;
30
+ /**
31
+ * Prompt user for API key
32
+ */
33
+ private promptForApiKey;
34
+ /**
35
+ * Prompt user for bearer token
36
+ */
37
+ private promptForBearerToken;
38
+ /**
39
+ * Perform OAuth2 authorization code flow
40
+ */
41
+ private performOAuth2Flow;
42
+ /**
43
+ * Register OAuth2 client dynamically
44
+ */
45
+ private registerClient;
46
+ /**
47
+ * Start local callback server and initiate authorization
48
+ */
49
+ private startCallbackServerAndAuthorize;
50
+ /**
51
+ * Generate PKCE code verifier and challenge
52
+ */
53
+ private generatePKCE;
54
+ /**
55
+ * Build the OAuth2 authorization URL
56
+ */
57
+ private buildAuthorizationUrl;
58
+ /**
59
+ * Exchange authorization code for access token
60
+ */
61
+ private exchangeCodeForToken;
62
+ /**
63
+ * Set access token directly (for testing or manual token entry)
64
+ */
65
+ setAccessToken(token: string): void;
66
+ }
67
+ //# sourceMappingURL=auth-manager.d.ts.map
@@ -0,0 +1,308 @@
1
+ import express from 'express';
2
+ import open from 'open';
3
+ import crypto from 'crypto';
4
+ import { createServer } from 'http';
5
+ import { AuthenticationError } from './errors.js';
6
+ import * as readline from 'readline';
7
+ const CALLBACK_PORT = 54321;
8
+ const CALLBACK_PATH = '/callback';
9
+ export class AuthManager {
10
+ authConfig;
11
+ accessToken;
12
+ refreshToken;
13
+ clientId;
14
+ clientSecret;
15
+ codeVerifier;
16
+ constructor(authConfig) {
17
+ this.authConfig = authConfig;
18
+ }
19
+ /**
20
+ * Check if authentication is required
21
+ */
22
+ requiresAuth() {
23
+ return this.authConfig.type !== 'none';
24
+ }
25
+ /**
26
+ * Get the current access token
27
+ */
28
+ getAccessToken() {
29
+ return this.accessToken;
30
+ }
31
+ /**
32
+ * Get authorization headers for requests
33
+ */
34
+ getAuthHeaders() {
35
+ if (!this.accessToken) {
36
+ return {};
37
+ }
38
+ switch (this.authConfig.type) {
39
+ case 'oauth2':
40
+ case 'bearer':
41
+ return { Authorization: `Bearer ${this.accessToken}` };
42
+ case 'apiKey':
43
+ if (this.authConfig.apiKeyIn === 'header' && this.authConfig.apiKeyHeader) {
44
+ return { [this.authConfig.apiKeyHeader]: this.accessToken };
45
+ }
46
+ return {};
47
+ default:
48
+ return {};
49
+ }
50
+ }
51
+ /**
52
+ * Get query parameters for API key auth
53
+ */
54
+ getAuthQueryParams() {
55
+ if (this.authConfig.type === 'apiKey' &&
56
+ this.authConfig.apiKeyIn === 'query' &&
57
+ this.authConfig.apiKeyHeader &&
58
+ this.accessToken) {
59
+ return { [this.authConfig.apiKeyHeader]: this.accessToken };
60
+ }
61
+ return {};
62
+ }
63
+ /**
64
+ * Perform authentication based on config type
65
+ */
66
+ async authenticate() {
67
+ switch (this.authConfig.type) {
68
+ case 'oauth2':
69
+ await this.performOAuth2Flow();
70
+ break;
71
+ case 'apiKey':
72
+ await this.promptForApiKey();
73
+ break;
74
+ case 'bearer':
75
+ await this.promptForBearerToken();
76
+ break;
77
+ case 'none':
78
+ // No authentication needed
79
+ break;
80
+ }
81
+ }
82
+ /**
83
+ * Prompt user for API key
84
+ */
85
+ async promptForApiKey() {
86
+ const rl = readline.createInterface({
87
+ input: process.stdin,
88
+ output: process.stdout,
89
+ });
90
+ const headerName = this.authConfig.apiKeyHeader || 'API-Key';
91
+ this.accessToken = await new Promise((resolve) => {
92
+ rl.question(`Enter your API key (${headerName}): `, (answer) => {
93
+ rl.close();
94
+ resolve(answer.trim());
95
+ });
96
+ });
97
+ if (!this.accessToken) {
98
+ throw new AuthenticationError('API key is required');
99
+ }
100
+ }
101
+ /**
102
+ * Prompt user for bearer token
103
+ */
104
+ async promptForBearerToken() {
105
+ const rl = readline.createInterface({
106
+ input: process.stdin,
107
+ output: process.stdout,
108
+ });
109
+ this.accessToken = await new Promise((resolve) => {
110
+ rl.question('Enter your Bearer token: ', (answer) => {
111
+ rl.close();
112
+ resolve(answer.trim());
113
+ });
114
+ });
115
+ if (!this.accessToken) {
116
+ throw new AuthenticationError('Bearer token is required');
117
+ }
118
+ }
119
+ /**
120
+ * Perform OAuth2 authorization code flow
121
+ */
122
+ async performOAuth2Flow() {
123
+ if (!this.authConfig.authorizationUrl || !this.authConfig.tokenUrl) {
124
+ throw new AuthenticationError('OAuth2 requires authorizationUrl and tokenUrl');
125
+ }
126
+ // Register client dynamically
127
+ await this.registerClient();
128
+ // Start local callback server
129
+ const authCode = await this.startCallbackServerAndAuthorize();
130
+ // Exchange code for token
131
+ await this.exchangeCodeForToken(authCode);
132
+ }
133
+ /**
134
+ * Register OAuth2 client dynamically
135
+ */
136
+ async registerClient() {
137
+ // Derive registration URL from token URL (replace /token with /register)
138
+ const registrationUrl = this.authConfig.tokenUrl.replace(/\/token$/, '/register');
139
+ const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
140
+ console.log('Registering OAuth2 client...');
141
+ const response = await fetch(registrationUrl, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ },
146
+ body: JSON.stringify({
147
+ client_name: 'OpenAPI Agent CLI',
148
+ redirect_uris: [redirectUri],
149
+ grant_types: ['authorization_code'],
150
+ token_endpoint_auth_method: 'none',
151
+ }),
152
+ });
153
+ if (!response.ok) {
154
+ const errorText = await response.text();
155
+ throw new AuthenticationError(`Client registration failed: ${response.status} ${errorText}`);
156
+ }
157
+ const registration = (await response.json());
158
+ this.clientId = registration.client_id;
159
+ this.clientSecret = registration.client_secret;
160
+ console.log('Client registered successfully.');
161
+ }
162
+ /**
163
+ * Start local callback server and initiate authorization
164
+ */
165
+ async startCallbackServerAndAuthorize() {
166
+ return new Promise((resolve, reject) => {
167
+ const app = express();
168
+ let server;
169
+ app.get(CALLBACK_PATH, (req, res) => {
170
+ const code = req.query.code;
171
+ const error = req.query.error;
172
+ if (error) {
173
+ res.send(`
174
+ <html>
175
+ <body>
176
+ <h1>Authentication Failed</h1>
177
+ <p>Error: ${error}</p>
178
+ <p>You can close this window.</p>
179
+ </body>
180
+ </html>
181
+ `);
182
+ server.close();
183
+ reject(new AuthenticationError(error));
184
+ return;
185
+ }
186
+ if (!code) {
187
+ res.send(`
188
+ <html>
189
+ <body>
190
+ <h1>Authentication Failed</h1>
191
+ <p>No authorization code received.</p>
192
+ <p>You can close this window.</p>
193
+ </body>
194
+ </html>
195
+ `);
196
+ server.close();
197
+ reject(new AuthenticationError('No authorization code received'));
198
+ return;
199
+ }
200
+ res.send(`
201
+ <html>
202
+ <body>
203
+ <h1>Authentication Successful!</h1>
204
+ <p>You can close this window and return to the CLI.</p>
205
+ </body>
206
+ </html>
207
+ `);
208
+ server.close();
209
+ resolve(code);
210
+ });
211
+ server = createServer(app);
212
+ server.listen(CALLBACK_PORT, '127.0.0.1', () => {
213
+ const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
214
+ const authUrl = this.buildAuthorizationUrl(redirectUri);
215
+ console.log('\nAuthentication required. Opening browser...');
216
+ console.log(`If browser doesn't open, visit: ${authUrl}\n`);
217
+ open(authUrl).catch(() => {
218
+ console.log('Could not open browser automatically.');
219
+ });
220
+ });
221
+ server.on('error', (err) => {
222
+ if (err.code === 'EADDRINUSE') {
223
+ reject(new AuthenticationError(`Port ${CALLBACK_PORT} is already in use. Please free the port and try again.`));
224
+ }
225
+ else {
226
+ reject(new AuthenticationError(err.message));
227
+ }
228
+ });
229
+ // Timeout after 5 minutes
230
+ setTimeout(() => {
231
+ server.close();
232
+ reject(new AuthenticationError('Authentication timed out'));
233
+ }, 5 * 60 * 1000);
234
+ });
235
+ }
236
+ /**
237
+ * Generate PKCE code verifier and challenge
238
+ */
239
+ generatePKCE() {
240
+ // Generate random code verifier (43-128 characters)
241
+ const verifier = crypto.randomBytes(32).toString('base64url');
242
+ // Create code challenge using SHA-256
243
+ const challenge = crypto
244
+ .createHash('sha256')
245
+ .update(verifier)
246
+ .digest('base64url');
247
+ return { verifier, challenge };
248
+ }
249
+ /**
250
+ * Build the OAuth2 authorization URL
251
+ */
252
+ buildAuthorizationUrl(redirectUri) {
253
+ const url = new URL(this.authConfig.authorizationUrl);
254
+ // Generate PKCE
255
+ const pkce = this.generatePKCE();
256
+ this.codeVerifier = pkce.verifier;
257
+ url.searchParams.set('response_type', 'code');
258
+ url.searchParams.set('client_id', this.clientId);
259
+ url.searchParams.set('redirect_uri', redirectUri);
260
+ url.searchParams.set('code_challenge', pkce.challenge);
261
+ url.searchParams.set('code_challenge_method', 'S256');
262
+ if (this.authConfig.scopes && this.authConfig.scopes.length > 0) {
263
+ url.searchParams.set('scope', this.authConfig.scopes.join(' '));
264
+ }
265
+ // Generate state for CSRF protection
266
+ const state = crypto.randomBytes(16).toString('base64url');
267
+ url.searchParams.set('state', state);
268
+ return url.toString();
269
+ }
270
+ /**
271
+ * Exchange authorization code for access token
272
+ */
273
+ async exchangeCodeForToken(code) {
274
+ const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
275
+ const params = new URLSearchParams({
276
+ grant_type: 'authorization_code',
277
+ code,
278
+ redirect_uri: redirectUri,
279
+ client_id: this.clientId,
280
+ code_verifier: this.codeVerifier,
281
+ });
282
+ // Only include client_secret if set (public clients don't have one)
283
+ if (this.clientSecret) {
284
+ params.set('client_secret', this.clientSecret);
285
+ }
286
+ const response = await fetch(this.authConfig.tokenUrl, {
287
+ method: 'POST',
288
+ headers: {
289
+ 'Content-Type': 'application/x-www-form-urlencoded',
290
+ },
291
+ body: params.toString(),
292
+ });
293
+ if (!response.ok) {
294
+ const errorText = await response.text();
295
+ throw new AuthenticationError(`Token exchange failed: ${response.status} ${errorText}`);
296
+ }
297
+ const tokenResponse = (await response.json());
298
+ this.accessToken = tokenResponse.access_token;
299
+ this.refreshToken = tokenResponse.refresh_token;
300
+ }
301
+ /**
302
+ * Set access token directly (for testing or manual token entry)
303
+ */
304
+ setAccessToken(token) {
305
+ this.accessToken = token;
306
+ }
307
+ }
308
+ //# sourceMappingURL=auth-manager.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createCLI(): Command;
3
+ //# sourceMappingURL=cli.d.ts.map