@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/dist/cli.js ADDED
@@ -0,0 +1,315 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import * as readline from 'readline';
5
+ import { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
6
+ import { AuthManager } from './auth-manager.js';
7
+ import { createExecutableTools, executeToolByName } from './tool-executor.js';
8
+ import { Agent } from './agent.js';
9
+ import { UnsupportedSchemaError, AuthenticationError, ToolExecutionError, SpecLoadError, } from './errors.js';
10
+ const VERSION = '0.1.5';
11
+ export function createCLI() {
12
+ const program = new Command();
13
+ program
14
+ .name('spec2tools')
15
+ .description('Dynamically convert OpenAPI specs into AI agent tools at runtime')
16
+ .version(VERSION);
17
+ program
18
+ .command('start')
19
+ .description('Start the agent with an OpenAPI specification')
20
+ .requiredOption('-s, --spec <path>', 'Path or URL to OpenAPI specification')
21
+ .option('--no-auth', 'Skip authentication even if required by spec')
22
+ .option('--token <token>', 'Provide access token directly')
23
+ .action(async (options) => {
24
+ await startAgent(options);
25
+ });
26
+ return program;
27
+ }
28
+ async function startAgent(options) {
29
+ const spinner = ora();
30
+ try {
31
+ // Load OpenAPI spec
32
+ spinner.start('Loading OpenAPI specification...');
33
+ const spec = await loadOpenAPISpec(options.spec);
34
+ spinner.succeed(`Loaded: ${spec.info.title} v${spec.info.version}`);
35
+ // Extract base URL
36
+ const baseUrl = extractBaseUrl(spec);
37
+ console.log(chalk.dim(`Base URL: ${baseUrl}`));
38
+ // Extract auth config
39
+ const authConfig = extractAuthConfig(spec);
40
+ // Parse operations
41
+ spinner.start('Parsing operations...');
42
+ const toolDefs = parseOperations(spec);
43
+ spinner.succeed(`Found ${toolDefs.length} operations`);
44
+ if (toolDefs.length === 0) {
45
+ console.log(chalk.yellow('No operations found in the specification.'));
46
+ return;
47
+ }
48
+ // Initialize auth manager
49
+ const authManager = new AuthManager(authConfig);
50
+ // Handle authentication
51
+ if (options.token) {
52
+ authManager.setAccessToken(options.token);
53
+ console.log(chalk.green('Using provided access token'));
54
+ }
55
+ else if (options.auth && authManager.requiresAuth()) {
56
+ console.log(chalk.yellow(`\nAuthentication required (${authConfig.type})`));
57
+ await authManager.authenticate();
58
+ console.log(chalk.green('Authentication successful!'));
59
+ }
60
+ else if (authManager.requiresAuth()) {
61
+ console.log(chalk.yellow('\nWarning: API requires authentication but --no-auth was specified'));
62
+ }
63
+ // Create executable tools
64
+ const tools = createExecutableTools(toolDefs, baseUrl, authManager);
65
+ // Initialize session
66
+ const session = {
67
+ baseUrl,
68
+ tools,
69
+ authConfig,
70
+ accessToken: authManager.getAccessToken(),
71
+ };
72
+ // Start chat loop
73
+ await startChatLoop(session);
74
+ }
75
+ catch (error) {
76
+ spinner.fail();
77
+ if (error instanceof UnsupportedSchemaError) {
78
+ console.error(chalk.red(`\nSchema Error: ${error.message}`));
79
+ console.error(chalk.dim('This OpenAPI specification contains features that are not supported.'));
80
+ }
81
+ else if (error instanceof AuthenticationError) {
82
+ console.error(chalk.red(`\nAuth Error: ${error.message}`));
83
+ }
84
+ else if (error instanceof SpecLoadError) {
85
+ console.error(chalk.red(`\nSpec Error: ${error.message}`));
86
+ }
87
+ else {
88
+ console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : String(error)}`));
89
+ }
90
+ process.exit(1);
91
+ }
92
+ }
93
+ async function startChatLoop(session) {
94
+ // Initialize agent
95
+ const agent = new Agent({ tools: session.tools });
96
+ console.log(chalk.bold('\n--- Spec2Tools ---'));
97
+ console.log(chalk.dim('Type your message or use special commands:'));
98
+ console.log(chalk.dim(' /tools - List available tools'));
99
+ console.log(chalk.dim(' /call - Call a tool directly'));
100
+ console.log(chalk.dim(' /schema - Show tool schema'));
101
+ console.log(chalk.dim(' /help - Show help'));
102
+ console.log(chalk.dim(' /exit - Exit the CLI'));
103
+ console.log('');
104
+ const rl = readline.createInterface({
105
+ input: process.stdin,
106
+ output: process.stdout,
107
+ });
108
+ const prompt = () => {
109
+ rl.question(chalk.cyan('> '), async (input) => {
110
+ const trimmedInput = input.trim();
111
+ if (!trimmedInput) {
112
+ prompt();
113
+ return;
114
+ }
115
+ // Pause readline during async operations to prevent corruption
116
+ rl.pause();
117
+ try {
118
+ await handleInput(trimmedInput, session, agent);
119
+ }
120
+ catch (error) {
121
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
122
+ }
123
+ finally {
124
+ rl.resume();
125
+ prompt();
126
+ }
127
+ });
128
+ };
129
+ rl.on('close', () => {
130
+ console.log(chalk.dim('\nGoodbye!'));
131
+ process.exit(0);
132
+ });
133
+ prompt();
134
+ }
135
+ async function handleInput(input, session, agent) {
136
+ // Handle special commands
137
+ if (input.startsWith('/')) {
138
+ await handleCommand(input, session, agent);
139
+ return;
140
+ }
141
+ // Regular chat message
142
+ const spinner = ora('Thinking...').start();
143
+ try {
144
+ const response = await agent.chat(input);
145
+ spinner.stop();
146
+ console.log(chalk.white('\n' + response + '\n'));
147
+ }
148
+ catch (error) {
149
+ spinner.fail('Failed to get response');
150
+ throw error;
151
+ }
152
+ }
153
+ async function handleCommand(input, session, agent) {
154
+ const parts = input.slice(1).split(/\s+/);
155
+ const command = parts[0].toLowerCase();
156
+ const args = parts.slice(1);
157
+ switch (command) {
158
+ case 'tools':
159
+ listTools(session.tools);
160
+ break;
161
+ case 'call':
162
+ await callTool(session.tools, args);
163
+ break;
164
+ case 'schema':
165
+ showSchema(session.tools, args[0]);
166
+ break;
167
+ case 'help':
168
+ showHelp();
169
+ break;
170
+ case 'exit':
171
+ case 'quit':
172
+ console.log(chalk.dim('Goodbye!'));
173
+ process.exit(0);
174
+ case 'clear':
175
+ agent.clearHistory();
176
+ console.log(chalk.dim('Conversation history cleared.'));
177
+ break;
178
+ default:
179
+ console.log(chalk.yellow(`Unknown command: ${command}`));
180
+ console.log(chalk.dim('Type /help for available commands.'));
181
+ }
182
+ }
183
+ function listTools(tools) {
184
+ console.log(chalk.bold('\nAvailable tools:'));
185
+ tools.forEach((tool, index) => {
186
+ const signature = formatToolSignature(tool);
187
+ console.log(chalk.cyan(`${index + 1}. ${signature}`));
188
+ console.log(chalk.dim(` ${tool.description}`));
189
+ });
190
+ console.log('');
191
+ }
192
+ async function callTool(tools, args) {
193
+ if (args.length === 0) {
194
+ console.log(chalk.yellow('Usage: /call <toolName> [--param value ...]'));
195
+ console.log(chalk.dim('Example: /call createUser --name "John" --email "john@example.com"'));
196
+ return;
197
+ }
198
+ const toolName = args[0];
199
+ const tool = tools.find((t) => t.name === toolName);
200
+ if (!tool) {
201
+ console.log(chalk.red(`Tool not found: ${toolName}`));
202
+ console.log(chalk.dim('Use /tools to see available tools.'));
203
+ return;
204
+ }
205
+ // Parse remaining args as --key value pairs
206
+ const params = parseCallArgs(args.slice(1));
207
+ const spinner = ora(`Calling ${toolName}...`).start();
208
+ try {
209
+ const result = await executeToolByName(tools, toolName, params);
210
+ spinner.succeed(`${toolName} completed`);
211
+ console.log(chalk.white('\nResult:'));
212
+ console.log(JSON.stringify(result, null, 2));
213
+ console.log('');
214
+ }
215
+ catch (error) {
216
+ spinner.fail(`${toolName} failed`);
217
+ if (error instanceof ToolExecutionError) {
218
+ console.error(chalk.red(error.message));
219
+ }
220
+ else {
221
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
222
+ }
223
+ }
224
+ }
225
+ function parseCallArgs(args) {
226
+ const params = {};
227
+ let i = 0;
228
+ while (i < args.length) {
229
+ const arg = args[i];
230
+ if (arg.startsWith('--')) {
231
+ const key = arg.slice(2);
232
+ const value = args[i + 1];
233
+ if (value === undefined) {
234
+ params[key] = true;
235
+ i++;
236
+ }
237
+ else if (value.startsWith('--')) {
238
+ params[key] = true;
239
+ i++;
240
+ }
241
+ else {
242
+ // Try to parse as JSON, number, or boolean
243
+ params[key] = parseValue(value);
244
+ i += 2;
245
+ }
246
+ }
247
+ else {
248
+ i++;
249
+ }
250
+ }
251
+ return params;
252
+ }
253
+ function parseValue(value) {
254
+ // Remove quotes if present
255
+ if ((value.startsWith('"') && value.endsWith('"')) ||
256
+ (value.startsWith("'") && value.endsWith("'"))) {
257
+ return value.slice(1, -1);
258
+ }
259
+ // Try parsing as number
260
+ const num = Number(value);
261
+ if (!isNaN(num)) {
262
+ return num;
263
+ }
264
+ // Check for boolean
265
+ if (value.toLowerCase() === 'true')
266
+ return true;
267
+ if (value.toLowerCase() === 'false')
268
+ return false;
269
+ // Try parsing as JSON
270
+ try {
271
+ return JSON.parse(value);
272
+ }
273
+ catch {
274
+ return value;
275
+ }
276
+ }
277
+ function showSchema(tools, toolName) {
278
+ if (!toolName) {
279
+ console.log(chalk.yellow('Usage: /schema <toolName>'));
280
+ return;
281
+ }
282
+ const tool = tools.find((t) => t.name === toolName);
283
+ if (!tool) {
284
+ console.log(chalk.red(`Tool not found: ${toolName}`));
285
+ console.log(chalk.dim('Use /tools to see available tools.'));
286
+ return;
287
+ }
288
+ console.log(chalk.bold(`\nSchema for ${toolName}:`));
289
+ console.log(chalk.white(formatToolSchema(tool)));
290
+ console.log('');
291
+ }
292
+ function showHelp() {
293
+ console.log(chalk.bold('\nAgent CLI Help'));
294
+ console.log('');
295
+ console.log(chalk.cyan('Chat Mode:'));
296
+ console.log(' Just type your message to chat with the AI agent.');
297
+ console.log(' The agent can use available tools to help you.');
298
+ console.log('');
299
+ console.log(chalk.cyan('Special Commands:'));
300
+ console.log(chalk.white(' /tools') + chalk.dim(' - List all available tools'));
301
+ console.log(chalk.white(' /call <tool> [args]') +
302
+ chalk.dim(' - Call a tool directly'));
303
+ console.log(chalk.white(' /schema <tool>') + chalk.dim(' - Show tool parameter schema'));
304
+ console.log(chalk.white(' /clear') + chalk.dim(' - Clear conversation history'));
305
+ console.log(chalk.white(' /help') + chalk.dim(' - Show this help message'));
306
+ console.log(chalk.white(' /exit') + chalk.dim(' - Exit the CLI'));
307
+ console.log('');
308
+ console.log(chalk.cyan('Examples:'));
309
+ console.log(chalk.dim(' > What can you do?'));
310
+ console.log(chalk.dim(' > Create a user named John with email john@example.com'));
311
+ console.log(chalk.dim(' > /call createUser --name "John" --email "john@example.com"'));
312
+ console.log(chalk.dim(' > /schema createUser'));
313
+ console.log('');
314
+ }
315
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Error thrown when encountering unsupported OpenAPI schema features
3
+ */
4
+ export declare class UnsupportedSchemaError extends Error {
5
+ constructor(path: string, reason: string);
6
+ }
7
+ /**
8
+ * Error thrown when authentication fails
9
+ */
10
+ export declare class AuthenticationError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /**
14
+ * Error thrown when tool execution fails
15
+ */
16
+ export declare class ToolExecutionError extends Error {
17
+ readonly toolName: string;
18
+ readonly cause: Error;
19
+ constructor(toolName: string, cause: Error);
20
+ }
21
+ /**
22
+ * Error thrown when OpenAPI spec is invalid or cannot be fetched
23
+ */
24
+ export declare class SpecLoadError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Error thrown when encountering unsupported OpenAPI schema features
3
+ */
4
+ export class UnsupportedSchemaError extends Error {
5
+ constructor(path, reason) {
6
+ super(`Unsupported schema at ${path}: ${reason}`);
7
+ this.name = 'UnsupportedSchemaError';
8
+ }
9
+ }
10
+ /**
11
+ * Error thrown when authentication fails
12
+ */
13
+ export class AuthenticationError extends Error {
14
+ constructor(message) {
15
+ super(`Authentication failed: ${message}`);
16
+ this.name = 'AuthenticationError';
17
+ }
18
+ }
19
+ /**
20
+ * Error thrown when tool execution fails
21
+ */
22
+ export class ToolExecutionError extends Error {
23
+ toolName;
24
+ cause;
25
+ constructor(toolName, cause) {
26
+ super(`Tool ${toolName} failed: ${cause.message}`);
27
+ this.name = 'ToolExecutionError';
28
+ this.toolName = toolName;
29
+ this.cause = cause;
30
+ }
31
+ }
32
+ /**
33
+ * Error thrown when OpenAPI spec is invalid or cannot be fetched
34
+ */
35
+ export class SpecLoadError extends Error {
36
+ constructor(message) {
37
+ super(`Failed to load OpenAPI spec: ${message}`);
38
+ this.name = 'SpecLoadError';
39
+ }
40
+ }
41
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,6 @@
1
+ export { UnsupportedSchemaError, AuthenticationError, ToolExecutionError, SpecLoadError, } from './errors.js';
2
+ export type { HttpMethod, Tool, AuthType, AuthConfig, Session, OpenAPISpec, PathItem, Operation, Parameter, RequestBody, MediaType, Response, SchemaObject, SecurityScheme, } from './types.js';
3
+ export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
4
+ export { AuthManager } from './auth-manager.js';
5
+ export { createExecutableTools, executeToolByName } from './tool-executor.js';
6
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Error classes
2
+ export { UnsupportedSchemaError, AuthenticationError, ToolExecutionError, SpecLoadError, } from './errors.js';
3
+ // OpenAPI Parser
4
+ export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
5
+ // Auth Manager
6
+ export { AuthManager } from './auth-manager.js';
7
+ // Tool Executor
8
+ export { createExecutableTools, executeToolByName } from './tool-executor.js';
9
+ //# sourceMappingURL=index.js.map
package/dist/lib.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { generateText } from 'ai';
2
+ type ToolSet = NonNullable<Parameters<typeof generateText>[0]['tools']>;
3
+ export interface Spec2ToolsOptions {
4
+ /** Path or URL to OpenAPI specification */
5
+ spec: string;
6
+ }
7
+ /**
8
+ * Create AI SDK tools from an OpenAPI specification.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createTools } from 'spec2tools';
13
+ * import { generateText } from 'ai';
14
+ * import { openai } from '@ai-sdk/openai';
15
+ *
16
+ * const tools = await createTools({ spec: './openapi.yaml' });
17
+ *
18
+ * const result = await generateText({
19
+ * model: openai('gpt-4o'),
20
+ * tools,
21
+ * prompt: 'List all users',
22
+ * });
23
+ * ```
24
+ *
25
+ * @throws Error if the API requires authentication
26
+ */
27
+ export declare function createTools(options: Spec2ToolsOptions): Promise<ToolSet>;
28
+ export {};
29
+ //# sourceMappingURL=lib.d.ts.map
package/dist/lib.js ADDED
@@ -0,0 +1,99 @@
1
+ import { tool } from 'ai';
2
+ import { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, } from './openapi-parser.js';
3
+ import { AuthManager } from './auth-manager.js';
4
+ import { ToolExecutionError } from './errors.js';
5
+ /**
6
+ * Create AI SDK tools from an OpenAPI specification.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createTools } from 'spec2tools';
11
+ * import { generateText } from 'ai';
12
+ * import { openai } from '@ai-sdk/openai';
13
+ *
14
+ * const tools = await createTools({ spec: './openapi.yaml' });
15
+ *
16
+ * const result = await generateText({
17
+ * model: openai('gpt-4o'),
18
+ * tools,
19
+ * prompt: 'List all users',
20
+ * });
21
+ * ```
22
+ *
23
+ * @throws Error if the API requires authentication
24
+ */
25
+ export async function createTools(options) {
26
+ const spec = await loadOpenAPISpec(options.spec);
27
+ const baseUrl = extractBaseUrl(spec);
28
+ const authConfig = extractAuthConfig(spec);
29
+ // Check if auth is required
30
+ if (authConfig.type !== 'none') {
31
+ throw new Error(`This API requires authentication (${authConfig.type}). ` +
32
+ `The createTools() function only supports APIs without authentication. ` +
33
+ `Use the CLI for authenticated APIs: npx spec2tools start --spec ${options.spec}`);
34
+ }
35
+ const toolDefs = parseOperations(spec);
36
+ const authManager = new AuthManager(authConfig);
37
+ // Build AI SDK tools
38
+ const tools = {};
39
+ for (const toolDef of toolDefs) {
40
+ const { name, description, parameters, httpMethod, path } = toolDef;
41
+ tools[name] = tool({
42
+ description,
43
+ inputSchema: parameters,
44
+ execute: async (params) => {
45
+ // Build URL with path parameters
46
+ let url = `${baseUrl}${path}`;
47
+ const queryParams = {};
48
+ const bodyParams = {};
49
+ for (const [key, value] of Object.entries(params)) {
50
+ if (value === undefined)
51
+ continue;
52
+ if (url.includes(`{${key}}`)) {
53
+ // Path parameter
54
+ url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
55
+ }
56
+ else if (httpMethod === 'GET' || httpMethod === 'DELETE') {
57
+ // Query parameter for GET/DELETE
58
+ queryParams[key] = String(value);
59
+ }
60
+ else {
61
+ // Body parameter for POST/PUT/PATCH
62
+ bodyParams[key] = value;
63
+ }
64
+ }
65
+ // Add query parameters
66
+ const queryString = new URLSearchParams(queryParams).toString();
67
+ if (queryString) {
68
+ url += `?${queryString}`;
69
+ }
70
+ // Build request options
71
+ const fetchOptions = {
72
+ method: httpMethod,
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ ...authManager.getAuthHeaders(),
76
+ },
77
+ };
78
+ // Add body for non-GET/DELETE requests
79
+ if (Object.keys(bodyParams).length > 0 &&
80
+ httpMethod !== 'GET' &&
81
+ httpMethod !== 'DELETE') {
82
+ fetchOptions.body = JSON.stringify(bodyParams);
83
+ }
84
+ const response = await fetch(url, fetchOptions);
85
+ if (!response.ok) {
86
+ const errorText = await response.text();
87
+ throw new ToolExecutionError(name, new Error(`HTTP ${response.status}: ${errorText}`));
88
+ }
89
+ const contentType = response.headers.get('content-type');
90
+ if (contentType?.includes('application/json')) {
91
+ return await response.json();
92
+ }
93
+ return await response.text();
94
+ },
95
+ });
96
+ }
97
+ return tools;
98
+ }
99
+ //# sourceMappingURL=lib.js.map
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { createTools, type Spec2ToolsOptions } from './lib.js';
2
+ //# sourceMappingURL=main.d.ts.map
package/dist/main.js ADDED
@@ -0,0 +1,3 @@
1
+ // Library exports
2
+ export { createTools } from './lib.js';
3
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,26 @@
1
+ import { OpenAPISpec, Tool, AuthConfig } from './types.js';
2
+ /**
3
+ * Fetch and parse an OpenAPI specification from URL or file path
4
+ */
5
+ export declare function loadOpenAPISpec(specPath: string): Promise<OpenAPISpec>;
6
+ /**
7
+ * Extract the base URL from the OpenAPI spec
8
+ */
9
+ export declare function extractBaseUrl(spec: OpenAPISpec): string;
10
+ /**
11
+ * Extract authentication configuration from security schemes
12
+ */
13
+ export declare function extractAuthConfig(spec: OpenAPISpec): AuthConfig;
14
+ /**
15
+ * Parse all operations from the OpenAPI spec and generate tool definitions
16
+ */
17
+ export declare function parseOperations(spec: OpenAPISpec): Omit<Tool, 'execute'>[];
18
+ /**
19
+ * Format tool schema for display
20
+ */
21
+ export declare function formatToolSchema(tool: Omit<Tool, 'execute'>): string;
22
+ /**
23
+ * Format tool signature for display
24
+ */
25
+ export declare function formatToolSignature(tool: Omit<Tool, 'execute'>): string;
26
+ //# sourceMappingURL=openapi-parser.d.ts.map