@lantos1618/better-ui 0.9.0 → 0.9.1

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.
@@ -1,30 +1,8 @@
1
- import { T as Tool } from '../tool-yZTixiN2.mjs';
1
+ import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.mjs';
2
2
  import { J as JsonSchema } from '../schema-DdZf6066.mjs';
3
3
  import 'zod';
4
4
  import 'react';
5
5
 
6
- /**
7
- * OpenAPI 3.1 spec generator for Better UI tools.
8
- *
9
- * Converts a tool registry into a valid OpenAPI document.
10
- * Each tool becomes a POST endpoint at `/api/tools/{toolName}`.
11
- *
12
- * @example
13
- * ```typescript
14
- * import { generateOpenAPISpec } from '@lantos1618/better-ui/openapi';
15
- * import { tools } from './tools';
16
- *
17
- * const spec = generateOpenAPISpec({
18
- * title: 'My AI Tools API',
19
- * version: '1.0.0',
20
- * tools,
21
- * });
22
- *
23
- * // Serve as JSON
24
- * app.get('/openapi.json', (req, res) => res.json(spec));
25
- * ```
26
- */
27
-
28
6
  interface OpenAPISpecConfig {
29
7
  /** API title */
30
8
  title: string;
@@ -109,5 +87,36 @@ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
109
87
  * ```
110
88
  */
111
89
  declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
90
+ interface ToolRouterConfig {
91
+ /** Tool registry */
92
+ tools: Record<string, Tool>;
93
+ /** Base path prefix (default: '/api/tools') */
94
+ basePath?: string;
95
+ /** Optional context passed to every tool execution */
96
+ context?: Partial<ToolContext>;
97
+ /** Called before tool execution — throw to reject */
98
+ onBeforeExecute?: (toolName: string, input: unknown, req: Request) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * Create a request handler that routes to individual tool endpoints.
102
+ * Each tool is callable at `POST {basePath}/{toolName}`.
103
+ *
104
+ * Also serves the OpenAPI spec at `GET {basePath}` and a Swagger UI at `GET {basePath}/docs`.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Next.js catch-all: app/api/tools/[...path]/route.ts
109
+ * import { toolRouter } from '@lantos1618/better-ui/openapi';
110
+ * const router = toolRouter({ tools });
111
+ * export const GET = router;
112
+ * export const POST = router;
113
+ *
114
+ * // Now callable:
115
+ * // POST /api/tools/weather { "city": "Tokyo" } -> { "result": { "temp": 22, ... } }
116
+ * // GET /api/tools -> OpenAPI JSON spec
117
+ * // GET /api/tools/docs -> Swagger UI
118
+ * ```
119
+ */
120
+ declare function toolRouter(config: ToolRouterConfig): (req: Request) => Promise<Response>;
112
121
 
113
- export { type OpenAPISpec, type OpenAPISpecConfig, generateOpenAPISpec, openAPIHandler };
122
+ export { type OpenAPISpec, type OpenAPISpecConfig, type ToolRouterConfig, generateOpenAPISpec, openAPIHandler, toolRouter };
@@ -1,30 +1,8 @@
1
- import { T as Tool } from '../tool-yZTixiN2.js';
1
+ import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.js';
2
2
  import { J as JsonSchema } from '../schema-DdZf6066.js';
3
3
  import 'zod';
4
4
  import 'react';
5
5
 
6
- /**
7
- * OpenAPI 3.1 spec generator for Better UI tools.
8
- *
9
- * Converts a tool registry into a valid OpenAPI document.
10
- * Each tool becomes a POST endpoint at `/api/tools/{toolName}`.
11
- *
12
- * @example
13
- * ```typescript
14
- * import { generateOpenAPISpec } from '@lantos1618/better-ui/openapi';
15
- * import { tools } from './tools';
16
- *
17
- * const spec = generateOpenAPISpec({
18
- * title: 'My AI Tools API',
19
- * version: '1.0.0',
20
- * tools,
21
- * });
22
- *
23
- * // Serve as JSON
24
- * app.get('/openapi.json', (req, res) => res.json(spec));
25
- * ```
26
- */
27
-
28
6
  interface OpenAPISpecConfig {
29
7
  /** API title */
30
8
  title: string;
@@ -109,5 +87,36 @@ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
109
87
  * ```
110
88
  */
111
89
  declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
90
+ interface ToolRouterConfig {
91
+ /** Tool registry */
92
+ tools: Record<string, Tool>;
93
+ /** Base path prefix (default: '/api/tools') */
94
+ basePath?: string;
95
+ /** Optional context passed to every tool execution */
96
+ context?: Partial<ToolContext>;
97
+ /** Called before tool execution — throw to reject */
98
+ onBeforeExecute?: (toolName: string, input: unknown, req: Request) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * Create a request handler that routes to individual tool endpoints.
102
+ * Each tool is callable at `POST {basePath}/{toolName}`.
103
+ *
104
+ * Also serves the OpenAPI spec at `GET {basePath}` and a Swagger UI at `GET {basePath}/docs`.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Next.js catch-all: app/api/tools/[...path]/route.ts
109
+ * import { toolRouter } from '@lantos1618/better-ui/openapi';
110
+ * const router = toolRouter({ tools });
111
+ * export const GET = router;
112
+ * export const POST = router;
113
+ *
114
+ * // Now callable:
115
+ * // POST /api/tools/weather { "city": "Tokyo" } -> { "result": { "temp": 22, ... } }
116
+ * // GET /api/tools -> OpenAPI JSON spec
117
+ * // GET /api/tools/docs -> Swagger UI
118
+ * ```
119
+ */
120
+ declare function toolRouter(config: ToolRouterConfig): (req: Request) => Promise<Response>;
112
121
 
113
- export { type OpenAPISpec, type OpenAPISpecConfig, generateOpenAPISpec, openAPIHandler };
122
+ export { type OpenAPISpec, type OpenAPISpecConfig, type ToolRouterConfig, generateOpenAPISpec, openAPIHandler, toolRouter };
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var openapi_exports = {};
22
22
  __export(openapi_exports, {
23
23
  generateOpenAPISpec: () => generateOpenAPISpec,
24
- openAPIHandler: () => openAPIHandler
24
+ openAPIHandler: () => openAPIHandler,
25
+ toolRouter: () => toolRouter
25
26
  });
26
27
  module.exports = __toCommonJS(openapi_exports);
27
28
 
@@ -253,8 +254,86 @@ function openAPIHandler(config) {
253
254
  }
254
255
  });
255
256
  }
257
+ function toolRouter(config) {
258
+ const basePath = config.basePath ?? "/api/tools";
259
+ return async (req) => {
260
+ const url = new URL(req.url);
261
+ const path = url.pathname;
262
+ if (req.method === "OPTIONS") {
263
+ return new Response(null, {
264
+ headers: {
265
+ "Access-Control-Allow-Origin": "*",
266
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
267
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
268
+ }
269
+ });
270
+ }
271
+ if (req.method === "GET" && (path === basePath || path === basePath + "/")) {
272
+ const spec = generateOpenAPISpec({
273
+ title: "Tool API",
274
+ version: "1.0.0",
275
+ tools: config.tools,
276
+ basePath,
277
+ serverUrl: url.origin
278
+ });
279
+ return new Response(JSON.stringify(spec, null, 2), {
280
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
281
+ });
282
+ }
283
+ if (req.method === "GET" && (path === basePath + "/docs" || path === basePath + "/docs/")) {
284
+ const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>API Docs</title>
285
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
286
+ </head><body><div id="swagger-ui"></div>
287
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
288
+ <script>SwaggerUIBundle({url:'${basePath}',dom_id:'#swagger-ui',deepLinking:true});</script>
289
+ </body></html>`;
290
+ return new Response(html, {
291
+ headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
292
+ });
293
+ }
294
+ if (req.method === "POST" && path.startsWith(basePath + "/")) {
295
+ const toolName = path.slice(basePath.length + 1).replace(/\/$/, "");
296
+ if (!toolName) {
297
+ return Response.json({ error: "Missing tool name" }, { status: 400 });
298
+ }
299
+ if (!Object.prototype.hasOwnProperty.call(config.tools, toolName)) {
300
+ return Response.json({ error: "Tool not found" }, { status: 404 });
301
+ }
302
+ const tool = config.tools[toolName];
303
+ let input;
304
+ try {
305
+ input = await req.json();
306
+ } catch {
307
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
308
+ }
309
+ try {
310
+ if (config.onBeforeExecute) {
311
+ await config.onBeforeExecute(toolName, input, req);
312
+ }
313
+ const result = await tool.run(input, {
314
+ isServer: true,
315
+ headers: req.headers,
316
+ ...config.context
317
+ });
318
+ return Response.json({ result }, {
319
+ headers: { "Access-Control-Allow-Origin": "*" }
320
+ });
321
+ } catch (error) {
322
+ if (error instanceof Error && error.name === "ZodError") {
323
+ return Response.json({ error: error.message }, { status: 400 });
324
+ }
325
+ return Response.json(
326
+ { error: error instanceof Error ? error.message : "Tool execution failed" },
327
+ { status: 500 }
328
+ );
329
+ }
330
+ }
331
+ return Response.json({ error: "Not found" }, { status: 404 });
332
+ };
333
+ }
256
334
  // Annotate the CommonJS export names for ESM import in node:
257
335
  0 && (module.exports = {
258
336
  generateOpenAPISpec,
259
- openAPIHandler
337
+ openAPIHandler,
338
+ toolRouter
260
339
  });
@@ -91,7 +91,85 @@ function openAPIHandler(config) {
91
91
  }
92
92
  });
93
93
  }
94
+ function toolRouter(config) {
95
+ const basePath = config.basePath ?? "/api/tools";
96
+ return async (req) => {
97
+ const url = new URL(req.url);
98
+ const path = url.pathname;
99
+ if (req.method === "OPTIONS") {
100
+ return new Response(null, {
101
+ headers: {
102
+ "Access-Control-Allow-Origin": "*",
103
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
104
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
105
+ }
106
+ });
107
+ }
108
+ if (req.method === "GET" && (path === basePath || path === basePath + "/")) {
109
+ const spec = generateOpenAPISpec({
110
+ title: "Tool API",
111
+ version: "1.0.0",
112
+ tools: config.tools,
113
+ basePath,
114
+ serverUrl: url.origin
115
+ });
116
+ return new Response(JSON.stringify(spec, null, 2), {
117
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
118
+ });
119
+ }
120
+ if (req.method === "GET" && (path === basePath + "/docs" || path === basePath + "/docs/")) {
121
+ const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>API Docs</title>
122
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
123
+ </head><body><div id="swagger-ui"></div>
124
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
125
+ <script>SwaggerUIBundle({url:'${basePath}',dom_id:'#swagger-ui',deepLinking:true});</script>
126
+ </body></html>`;
127
+ return new Response(html, {
128
+ headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
129
+ });
130
+ }
131
+ if (req.method === "POST" && path.startsWith(basePath + "/")) {
132
+ const toolName = path.slice(basePath.length + 1).replace(/\/$/, "");
133
+ if (!toolName) {
134
+ return Response.json({ error: "Missing tool name" }, { status: 400 });
135
+ }
136
+ if (!Object.prototype.hasOwnProperty.call(config.tools, toolName)) {
137
+ return Response.json({ error: "Tool not found" }, { status: 404 });
138
+ }
139
+ const tool = config.tools[toolName];
140
+ let input;
141
+ try {
142
+ input = await req.json();
143
+ } catch {
144
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
145
+ }
146
+ try {
147
+ if (config.onBeforeExecute) {
148
+ await config.onBeforeExecute(toolName, input, req);
149
+ }
150
+ const result = await tool.run(input, {
151
+ isServer: true,
152
+ headers: req.headers,
153
+ ...config.context
154
+ });
155
+ return Response.json({ result }, {
156
+ headers: { "Access-Control-Allow-Origin": "*" }
157
+ });
158
+ } catch (error) {
159
+ if (error instanceof Error && error.name === "ZodError") {
160
+ return Response.json({ error: error.message }, { status: 400 });
161
+ }
162
+ return Response.json(
163
+ { error: error instanceof Error ? error.message : "Tool execution failed" },
164
+ { status: 500 }
165
+ );
166
+ }
167
+ }
168
+ return Response.json({ error: "Not found" }, { status: 404 });
169
+ };
170
+ }
94
171
  export {
95
172
  generateOpenAPISpec,
96
- openAPIHandler
173
+ openAPIHandler,
174
+ toolRouter
97
175
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lantos1618/better-ui",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "A minimal, type-safe AI-first UI framework for building tools",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",