@lantos1618/better-ui 0.7.1 → 0.9.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.
@@ -179,16 +179,24 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
179
179
  const [threads, setThreads] = (0, import_react2.useState)([]);
180
180
  const [activeThreadId, setActiveThreadId] = (0, import_react2.useState)(threadId);
181
181
  (0, import_react2.useEffect)(() => {
182
- if (persistence) {
183
- persistence.listThreads().then(setThreads).catch((err) => console.warn("[better-ui] persistence error:", err));
184
- }
182
+ if (!persistence) return;
183
+ let cancelled = false;
184
+ persistence.listThreads().then((t) => {
185
+ if (!cancelled) setThreads(t);
186
+ }).catch((err) => console.warn("[better-ui] persistence error:", err));
187
+ return () => {
188
+ cancelled = true;
189
+ };
185
190
  }, [persistence]);
186
191
  (0, import_react2.useEffect)(() => {
187
- if (persistence && activeThreadId) {
188
- persistence.getMessages(activeThreadId).then((msgs) => {
189
- setMessages(msgs);
190
- }).catch((err) => console.warn("[better-ui] persistence error:", err));
191
- }
192
+ if (!persistence || !activeThreadId) return;
193
+ let cancelled = false;
194
+ persistence.getMessages(activeThreadId).then((msgs) => {
195
+ if (!cancelled) setMessages(msgs);
196
+ }).catch((err) => console.warn("[better-ui] persistence error:", err));
197
+ return () => {
198
+ cancelled = true;
199
+ };
192
200
  }, [persistence, activeThreadId, setMessages]);
193
201
  const prevStatusRef = (0, import_react2.useRef)(status);
194
202
  (0, import_react2.useEffect)(() => {
@@ -212,9 +220,10 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
212
220
  }, [persistence, setMessages, toolStateStore]);
213
221
  const switchThreadFn = (0, import_react2.useCallback)(async (id) => {
214
222
  if (!persistence) throw new Error("Persistence not configured");
215
- setActiveThreadId(id);
216
- setMessages([]);
223
+ const msgs = await persistence.getMessages(id);
217
224
  toolStateStore.clear();
225
+ setMessages(msgs);
226
+ setActiveThreadId(id);
218
227
  }, [persistence, setMessages, toolStateStore]);
219
228
  const deleteThreadFn = (0, import_react2.useCallback)(async (id) => {
220
229
  if (!persistence) throw new Error("Persistence not configured");
@@ -237,8 +246,11 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
237
246
  stateContext[entry.toolName] = entry.output;
238
247
  }
239
248
  }
240
- dirty.clear();
241
- return Object.keys(stateContext).length > 0 ? stateContext : null;
249
+ if (Object.keys(stateContext).length > 0) {
250
+ dirty.clear();
251
+ return stateContext;
252
+ }
253
+ return null;
242
254
  }, [toolStateStore]);
243
255
  const executeToolDirect = (0, import_react2.useCallback)(async (toolName, toolInput, toolCallId) => {
244
256
  const currentVersion = (versionRef.current.get(toolCallId) || 0) + 1;
@@ -420,6 +432,33 @@ var import_ai2 = require("ai");
420
432
  // src/components/ToolResult.tsx
421
433
  var import_react4 = require("react");
422
434
  var import_jsx_runtime2 = require("react/jsx-runtime");
435
+ var ToolViewErrorBoundary = class extends import_react4.Component {
436
+ constructor() {
437
+ super(...arguments);
438
+ this.state = { error: null };
439
+ }
440
+ static getDerivedStateFromError(error) {
441
+ return { error };
442
+ }
443
+ componentDidCatch(error, info) {
444
+ console.error(`[better-ui] View for tool "${this.props.toolName}" threw:`, error, info);
445
+ }
446
+ render() {
447
+ if (this.state.error) {
448
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bg-[var(--bui-error-muted,rgba(220,38,38,0.08))] border border-[var(--bui-error-border,rgba(153,27,27,0.5))] rounded-xl p-4", children: [
449
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2", children: [
450
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "w-4 h-4 text-[var(--bui-error-fg,#f87171)] shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" }) }),
451
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "text-[var(--bui-error-fg,#f87171)] text-sm font-medium", children: [
452
+ this.props.toolName,
453
+ " view error"
454
+ ] })
455
+ ] }),
456
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-[var(--bui-error-fg,#f87171)]/70 text-xs mt-1", children: this.state.error.message })
457
+ ] });
458
+ }
459
+ return this.props.children;
460
+ }
461
+ };
423
462
  function useIsFollowup(toolStateStore, toolCallId) {
424
463
  const subscribe = (0, import_react4.useCallback)(
425
464
  (listener) => toolStateStore.subscribeAll(listener),
@@ -602,7 +641,7 @@ function ToolResult({
602
641
  )
603
642
  ] });
604
643
  }
605
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: className || "", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
644
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: className || "", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToolViewErrorBoundary, { toolName, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
606
645
  toolDef.View,
607
646
  {
608
647
  data: resolvedOutput,
@@ -610,7 +649,7 @@ function ToolResult({
610
649
  onAction: getOnAction(toolCallId, toolName),
611
650
  error: storeState?.error ? new Error(storeState.error) : null
612
651
  }
613
- ) });
652
+ ) }) });
614
653
  }
615
654
 
616
655
  // src/components/Markdown.tsx
@@ -121,16 +121,24 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
121
121
  const [threads, setThreads] = useState([]);
122
122
  const [activeThreadId, setActiveThreadId] = useState(threadId);
123
123
  useEffect(() => {
124
- if (persistence) {
125
- persistence.listThreads().then(setThreads).catch((err) => console.warn("[better-ui] persistence error:", err));
126
- }
124
+ if (!persistence) return;
125
+ let cancelled = false;
126
+ persistence.listThreads().then((t) => {
127
+ if (!cancelled) setThreads(t);
128
+ }).catch((err) => console.warn("[better-ui] persistence error:", err));
129
+ return () => {
130
+ cancelled = true;
131
+ };
127
132
  }, [persistence]);
128
133
  useEffect(() => {
129
- if (persistence && activeThreadId) {
130
- persistence.getMessages(activeThreadId).then((msgs) => {
131
- setMessages(msgs);
132
- }).catch((err) => console.warn("[better-ui] persistence error:", err));
133
- }
134
+ if (!persistence || !activeThreadId) return;
135
+ let cancelled = false;
136
+ persistence.getMessages(activeThreadId).then((msgs) => {
137
+ if (!cancelled) setMessages(msgs);
138
+ }).catch((err) => console.warn("[better-ui] persistence error:", err));
139
+ return () => {
140
+ cancelled = true;
141
+ };
134
142
  }, [persistence, activeThreadId, setMessages]);
135
143
  const prevStatusRef = useRef(status);
136
144
  useEffect(() => {
@@ -154,9 +162,10 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
154
162
  }, [persistence, setMessages, toolStateStore]);
155
163
  const switchThreadFn = useCallback2(async (id) => {
156
164
  if (!persistence) throw new Error("Persistence not configured");
157
- setActiveThreadId(id);
158
- setMessages([]);
165
+ const msgs = await persistence.getMessages(id);
159
166
  toolStateStore.clear();
167
+ setMessages(msgs);
168
+ setActiveThreadId(id);
160
169
  }, [persistence, setMessages, toolStateStore]);
161
170
  const deleteThreadFn = useCallback2(async (id) => {
162
171
  if (!persistence) throw new Error("Persistence not configured");
@@ -179,8 +188,11 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
179
188
  stateContext[entry.toolName] = entry.output;
180
189
  }
181
190
  }
182
- dirty.clear();
183
- return Object.keys(stateContext).length > 0 ? stateContext : null;
191
+ if (Object.keys(stateContext).length > 0) {
192
+ dirty.clear();
193
+ return stateContext;
194
+ }
195
+ return null;
184
196
  }, [toolStateStore]);
185
197
  const executeToolDirect = useCallback2(async (toolName, toolInput, toolCallId) => {
186
198
  const currentVersion = (versionRef.current.get(toolCallId) || 0) + 1;
@@ -364,8 +376,35 @@ import {
364
376
  } from "ai";
365
377
 
366
378
  // src/components/ToolResult.tsx
367
- import { useEffect as useEffect2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2, useCallback as useCallback3 } from "react";
379
+ import { Component, useEffect as useEffect2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2, useCallback as useCallback3 } from "react";
368
380
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
381
+ var ToolViewErrorBoundary = class extends Component {
382
+ constructor() {
383
+ super(...arguments);
384
+ this.state = { error: null };
385
+ }
386
+ static getDerivedStateFromError(error) {
387
+ return { error };
388
+ }
389
+ componentDidCatch(error, info) {
390
+ console.error(`[better-ui] View for tool "${this.props.toolName}" threw:`, error, info);
391
+ }
392
+ render() {
393
+ if (this.state.error) {
394
+ return /* @__PURE__ */ jsxs("div", { className: "bg-[var(--bui-error-muted,rgba(220,38,38,0.08))] border border-[var(--bui-error-border,rgba(153,27,27,0.5))] rounded-xl p-4", children: [
395
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
396
+ /* @__PURE__ */ jsx2("svg", { className: "w-4 h-4 text-[var(--bui-error-fg,#f87171)] shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx2("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" }) }),
397
+ /* @__PURE__ */ jsxs("span", { className: "text-[var(--bui-error-fg,#f87171)] text-sm font-medium", children: [
398
+ this.props.toolName,
399
+ " view error"
400
+ ] })
401
+ ] }),
402
+ /* @__PURE__ */ jsx2("p", { className: "text-[var(--bui-error-fg,#f87171)]/70 text-xs mt-1", children: this.state.error.message })
403
+ ] });
404
+ }
405
+ return this.props.children;
406
+ }
407
+ };
369
408
  function useIsFollowup(toolStateStore, toolCallId) {
370
409
  const subscribe = useCallback3(
371
410
  (listener) => toolStateStore.subscribeAll(listener),
@@ -548,7 +587,7 @@ function ToolResult({
548
587
  )
549
588
  ] });
550
589
  }
551
- return /* @__PURE__ */ jsx2("div", { className: className || "", children: /* @__PURE__ */ jsx2(
590
+ return /* @__PURE__ */ jsx2("div", { className: className || "", children: /* @__PURE__ */ jsx2(ToolViewErrorBoundary, { toolName, children: /* @__PURE__ */ jsx2(
552
591
  toolDef.View,
553
592
  {
554
593
  data: resolvedOutput,
@@ -556,7 +595,7 @@ function ToolResult({
556
595
  onAction: getOnAction(toolCallId, toolName),
557
596
  error: storeState?.error ? new Error(storeState.error) : null
558
597
  }
559
- ) });
598
+ ) }) });
560
599
  }
561
600
 
562
601
  // src/components/Markdown.tsx
@@ -1,5 +1,6 @@
1
1
  import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.mjs';
2
- import { z } from 'zod';
2
+ export { z as zodToJsonSchema } from '../schema-DdZf6066.mjs';
3
+ import 'zod';
3
4
  import 'react';
4
5
 
5
6
  /**
@@ -127,33 +128,4 @@ declare class MCPServer {
127
128
  */
128
129
  declare function createMCPServer(config: MCPServerConfig): MCPServer;
129
130
 
130
- /**
131
- * Lightweight Zod-to-JSON-Schema converter.
132
- * Handles common Zod types without requiring zod-to-json-schema dependency.
133
- */
134
-
135
- interface JsonSchema {
136
- type?: string;
137
- properties?: Record<string, JsonSchema>;
138
- required?: string[];
139
- items?: JsonSchema;
140
- enum?: unknown[];
141
- description?: string;
142
- default?: unknown;
143
- minimum?: number;
144
- maximum?: number;
145
- minLength?: number;
146
- maxLength?: number;
147
- pattern?: string;
148
- format?: string;
149
- anyOf?: JsonSchema[];
150
- oneOf?: JsonSchema[];
151
- nullable?: boolean;
152
- additionalProperties?: boolean | JsonSchema;
153
- [key: string]: unknown;
154
- }
155
- declare function zodToJsonSchema(schema: z.ZodType | {
156
- [key: string]: any;
157
- }): JsonSchema;
158
-
159
- export { MCPServer, type MCPServerConfig, createMCPServer, zodToJsonSchema };
131
+ export { MCPServer, type MCPServerConfig, createMCPServer };
@@ -1,5 +1,6 @@
1
1
  import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.js';
2
- import { z } from 'zod';
2
+ export { z as zodToJsonSchema } from '../schema-DdZf6066.js';
3
+ import 'zod';
3
4
  import 'react';
4
5
 
5
6
  /**
@@ -127,33 +128,4 @@ declare class MCPServer {
127
128
  */
128
129
  declare function createMCPServer(config: MCPServerConfig): MCPServer;
129
130
 
130
- /**
131
- * Lightweight Zod-to-JSON-Schema converter.
132
- * Handles common Zod types without requiring zod-to-json-schema dependency.
133
- */
134
-
135
- interface JsonSchema {
136
- type?: string;
137
- properties?: Record<string, JsonSchema>;
138
- required?: string[];
139
- items?: JsonSchema;
140
- enum?: unknown[];
141
- description?: string;
142
- default?: unknown;
143
- minimum?: number;
144
- maximum?: number;
145
- minLength?: number;
146
- maxLength?: number;
147
- pattern?: string;
148
- format?: string;
149
- anyOf?: JsonSchema[];
150
- oneOf?: JsonSchema[];
151
- nullable?: boolean;
152
- additionalProperties?: boolean | JsonSchema;
153
- [key: string]: unknown;
154
- }
155
- declare function zodToJsonSchema(schema: z.ZodType | {
156
- [key: string]: any;
157
- }): JsonSchema;
158
-
159
- export { MCPServer, type MCPServerConfig, createMCPServer, zodToJsonSchema };
131
+ export { MCPServer, type MCPServerConfig, createMCPServer };
package/dist/mcp/index.js CHANGED
@@ -296,7 +296,7 @@ var MCPServer = class {
296
296
  jsonrpc: "2.0",
297
297
  id: message.id,
298
298
  result: {
299
- protocolVersion: "2024-11-05",
299
+ protocolVersion: "2025-11-25",
300
300
  capabilities: {
301
301
  tools: {}
302
302
  },
@@ -133,7 +133,7 @@ var MCPServer = class {
133
133
  jsonrpc: "2.0",
134
134
  id: message.id,
135
135
  result: {
136
- protocolVersion: "2024-11-05",
136
+ protocolVersion: "2025-11-25",
137
137
  capabilities: {
138
138
  tools: {}
139
139
  },
@@ -0,0 +1,113 @@
1
+ import { T as Tool } from '../tool-yZTixiN2.mjs';
2
+ import { J as JsonSchema } from '../schema-DdZf6066.mjs';
3
+ import 'zod';
4
+ import 'react';
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
+ interface OpenAPISpecConfig {
29
+ /** API title */
30
+ title: string;
31
+ /** API version */
32
+ version: string;
33
+ /** Tool registry */
34
+ tools: Record<string, Tool>;
35
+ /** API description */
36
+ description?: string;
37
+ /** Base URL for servers (default: '/') */
38
+ serverUrl?: string;
39
+ /** Base path prefix for tool endpoints (default: '/api/tools') */
40
+ basePath?: string;
41
+ /** Tags to group endpoints */
42
+ tags?: Array<{
43
+ name: string;
44
+ description?: string;
45
+ }>;
46
+ }
47
+ interface OpenAPISpec {
48
+ openapi: '3.1.0';
49
+ info: {
50
+ title: string;
51
+ version: string;
52
+ description?: string;
53
+ };
54
+ servers: Array<{
55
+ url: string;
56
+ }>;
57
+ paths: Record<string, PathItem>;
58
+ components: {
59
+ schemas: Record<string, JsonSchema>;
60
+ };
61
+ tags?: Array<{
62
+ name: string;
63
+ description?: string;
64
+ }>;
65
+ }
66
+ interface PathItem {
67
+ post: {
68
+ operationId: string;
69
+ summary: string;
70
+ description?: string;
71
+ tags?: string[];
72
+ requestBody: {
73
+ required: true;
74
+ content: {
75
+ 'application/json': {
76
+ schema: {
77
+ $ref: string;
78
+ };
79
+ };
80
+ };
81
+ };
82
+ responses: Record<string, {
83
+ description: string;
84
+ content?: {
85
+ 'application/json': {
86
+ schema: {
87
+ $ref: string;
88
+ } | JsonSchema;
89
+ };
90
+ };
91
+ }>;
92
+ };
93
+ }
94
+ /**
95
+ * Generate an OpenAPI 3.1 spec from a Better UI tool registry.
96
+ */
97
+ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
98
+ /**
99
+ * Create a request handler that serves the OpenAPI spec as JSON.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Next.js route: app/api/openapi/route.ts
104
+ * export const GET = openAPIHandler({
105
+ * title: 'My Tools',
106
+ * version: '1.0.0',
107
+ * tools,
108
+ * });
109
+ * ```
110
+ */
111
+ declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
112
+
113
+ export { type OpenAPISpec, type OpenAPISpecConfig, generateOpenAPISpec, openAPIHandler };
@@ -0,0 +1,113 @@
1
+ import { T as Tool } from '../tool-yZTixiN2.js';
2
+ import { J as JsonSchema } from '../schema-DdZf6066.js';
3
+ import 'zod';
4
+ import 'react';
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
+ interface OpenAPISpecConfig {
29
+ /** API title */
30
+ title: string;
31
+ /** API version */
32
+ version: string;
33
+ /** Tool registry */
34
+ tools: Record<string, Tool>;
35
+ /** API description */
36
+ description?: string;
37
+ /** Base URL for servers (default: '/') */
38
+ serverUrl?: string;
39
+ /** Base path prefix for tool endpoints (default: '/api/tools') */
40
+ basePath?: string;
41
+ /** Tags to group endpoints */
42
+ tags?: Array<{
43
+ name: string;
44
+ description?: string;
45
+ }>;
46
+ }
47
+ interface OpenAPISpec {
48
+ openapi: '3.1.0';
49
+ info: {
50
+ title: string;
51
+ version: string;
52
+ description?: string;
53
+ };
54
+ servers: Array<{
55
+ url: string;
56
+ }>;
57
+ paths: Record<string, PathItem>;
58
+ components: {
59
+ schemas: Record<string, JsonSchema>;
60
+ };
61
+ tags?: Array<{
62
+ name: string;
63
+ description?: string;
64
+ }>;
65
+ }
66
+ interface PathItem {
67
+ post: {
68
+ operationId: string;
69
+ summary: string;
70
+ description?: string;
71
+ tags?: string[];
72
+ requestBody: {
73
+ required: true;
74
+ content: {
75
+ 'application/json': {
76
+ schema: {
77
+ $ref: string;
78
+ };
79
+ };
80
+ };
81
+ };
82
+ responses: Record<string, {
83
+ description: string;
84
+ content?: {
85
+ 'application/json': {
86
+ schema: {
87
+ $ref: string;
88
+ } | JsonSchema;
89
+ };
90
+ };
91
+ }>;
92
+ };
93
+ }
94
+ /**
95
+ * Generate an OpenAPI 3.1 spec from a Better UI tool registry.
96
+ */
97
+ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
98
+ /**
99
+ * Create a request handler that serves the OpenAPI spec as JSON.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Next.js route: app/api/openapi/route.ts
104
+ * export const GET = openAPIHandler({
105
+ * title: 'My Tools',
106
+ * version: '1.0.0',
107
+ * tools,
108
+ * });
109
+ * ```
110
+ */
111
+ declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
112
+
113
+ export { type OpenAPISpec, type OpenAPISpecConfig, generateOpenAPISpec, openAPIHandler };
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/openapi/index.ts
21
+ var openapi_exports = {};
22
+ __export(openapi_exports, {
23
+ generateOpenAPISpec: () => generateOpenAPISpec,
24
+ openAPIHandler: () => openAPIHandler
25
+ });
26
+ module.exports = __toCommonJS(openapi_exports);
27
+
28
+ // src/mcp/schema.ts
29
+ function zodToJsonSchema(schema) {
30
+ return convert(schema);
31
+ }
32
+ function convert(schema) {
33
+ const def = schema._def;
34
+ const typeName = def?.typeName;
35
+ switch (typeName) {
36
+ case "ZodString":
37
+ return convertString(def);
38
+ case "ZodNumber":
39
+ return convertNumber(def);
40
+ case "ZodBoolean":
41
+ return { type: "boolean" };
42
+ case "ZodNull":
43
+ return { type: "null" };
44
+ case "ZodLiteral":
45
+ return { enum: [def.value] };
46
+ case "ZodEnum":
47
+ return { type: "string", enum: def.values };
48
+ case "ZodNativeEnum":
49
+ return { enum: Object.values(def.values) };
50
+ case "ZodObject":
51
+ return convertObject(def);
52
+ case "ZodArray":
53
+ return convertArray(def);
54
+ case "ZodOptional":
55
+ return convert(def.innerType);
56
+ case "ZodNullable": {
57
+ const inner = convert(def.innerType);
58
+ return { anyOf: [inner, { type: "null" }] };
59
+ }
60
+ case "ZodDefault":
61
+ return { ...convert(def.innerType), default: def.defaultValue() };
62
+ case "ZodUnion":
63
+ return { anyOf: def.options.map((o) => convert(o)) };
64
+ case "ZodDiscriminatedUnion":
65
+ return { oneOf: [...def.options.values()].map((o) => convert(o)) };
66
+ case "ZodRecord":
67
+ return {
68
+ type: "object",
69
+ additionalProperties: convert(def.valueType)
70
+ };
71
+ case "ZodTuple": {
72
+ const items = def.items.map((item) => convert(item));
73
+ return { type: "array", items: items.length === 1 ? items[0] : void 0, prefixItems: items };
74
+ }
75
+ case "ZodEffects":
76
+ return convert(def.schema);
77
+ case "ZodPipeline":
78
+ return convert(def.in);
79
+ case "ZodLazy":
80
+ return convert(def.getter());
81
+ case "ZodAny":
82
+ return {};
83
+ case "ZodUnknown":
84
+ return {};
85
+ default:
86
+ return {};
87
+ }
88
+ }
89
+ function convertString(def) {
90
+ const schema = { type: "string" };
91
+ if (def.checks) {
92
+ for (const check of def.checks) {
93
+ switch (check.kind) {
94
+ case "min":
95
+ schema.minLength = check.value;
96
+ break;
97
+ case "max":
98
+ schema.maxLength = check.value;
99
+ break;
100
+ case "email":
101
+ schema.format = "email";
102
+ break;
103
+ case "url":
104
+ schema.format = "uri";
105
+ break;
106
+ case "uuid":
107
+ schema.format = "uuid";
108
+ break;
109
+ case "regex":
110
+ schema.pattern = check.regex.source;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ if (def.description) schema.description = def.description;
116
+ return schema;
117
+ }
118
+ function convertNumber(def) {
119
+ const schema = { type: "number" };
120
+ if (def.checks) {
121
+ for (const check of def.checks) {
122
+ switch (check.kind) {
123
+ case "min":
124
+ schema.minimum = check.value;
125
+ if (check.inclusive === false) schema.exclusiveMinimum = check.value;
126
+ break;
127
+ case "max":
128
+ schema.maximum = check.value;
129
+ if (check.inclusive === false) schema.exclusiveMaximum = check.value;
130
+ break;
131
+ case "int":
132
+ schema.type = "integer";
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ if (def.description) schema.description = def.description;
138
+ return schema;
139
+ }
140
+ function convertObject(def) {
141
+ const shape = def.shape();
142
+ const properties = {};
143
+ const required = [];
144
+ for (const [key, value] of Object.entries(shape)) {
145
+ properties[key] = convert(value);
146
+ const fieldDef = value._def;
147
+ const isOptional = fieldDef?.typeName === "ZodOptional" || fieldDef?.typeName === "ZodDefault";
148
+ if (!isOptional) {
149
+ required.push(key);
150
+ }
151
+ }
152
+ const schema = { type: "object", properties };
153
+ if (required.length > 0) schema.required = required;
154
+ if (def.description) schema.description = def.description;
155
+ return schema;
156
+ }
157
+ function convertArray(def) {
158
+ const schema = {
159
+ type: "array",
160
+ items: convert(def.type)
161
+ };
162
+ if (def.minLength) schema.minItems = def.minLength.value;
163
+ if (def.maxLength) schema.maxItems = def.maxLength.value;
164
+ if (def.description) schema.description = def.description;
165
+ return schema;
166
+ }
167
+
168
+ // src/openapi/index.ts
169
+ function generateOpenAPISpec(config) {
170
+ const basePath = config.basePath ?? "/api/tools";
171
+ const paths = {};
172
+ const schemas = {};
173
+ const autoTags = /* @__PURE__ */ new Set();
174
+ for (const [key, tool] of Object.entries(config.tools)) {
175
+ const name = tool.name || key;
176
+ const inputSchemaName = `${name}Input`;
177
+ const outputSchemaName = `${name}Output`;
178
+ schemas[inputSchemaName] = zodToJsonSchema(tool.inputSchema);
179
+ if (tool.outputSchema) {
180
+ schemas[outputSchemaName] = zodToJsonSchema(tool.outputSchema);
181
+ }
182
+ const toolTags = tool.tags.length > 0 ? tool.tags : [name];
183
+ for (const t of toolTags) autoTags.add(t);
184
+ const responseSchema = tool.outputSchema ? { $ref: `#/components/schemas/${outputSchemaName}` } : { type: "object" };
185
+ const hints = [];
186
+ if (tool.hints.destructive) hints.push("destructive");
187
+ if (tool.hints.readOnly) hints.push("read-only");
188
+ if (tool.hints.idempotent) hints.push("idempotent");
189
+ if (tool.requiresConfirmation) hints.push("requires confirmation");
190
+ const hintsStr = hints.length > 0 ? ` [${hints.join(", ")}]` : "";
191
+ paths[`${basePath}/${name}`] = {
192
+ post: {
193
+ operationId: name,
194
+ summary: tool.description || name,
195
+ description: (tool.description || "") + hintsStr,
196
+ tags: tool.tags.length > 0 ? tool.tags : void 0,
197
+ requestBody: {
198
+ required: true,
199
+ content: {
200
+ "application/json": {
201
+ schema: { $ref: `#/components/schemas/${inputSchemaName}` }
202
+ }
203
+ }
204
+ },
205
+ responses: {
206
+ "200": {
207
+ description: "Successful tool execution",
208
+ content: {
209
+ "application/json": {
210
+ schema: {
211
+ type: "object",
212
+ properties: {
213
+ result: responseSchema
214
+ }
215
+ }
216
+ }
217
+ }
218
+ },
219
+ "400": {
220
+ description: "Invalid input (Zod validation error)"
221
+ },
222
+ "404": {
223
+ description: "Tool not found"
224
+ },
225
+ "500": {
226
+ description: "Tool execution failed"
227
+ }
228
+ }
229
+ }
230
+ };
231
+ }
232
+ const tags = config.tags ?? [...autoTags].map((t) => ({ name: t }));
233
+ return {
234
+ openapi: "3.1.0",
235
+ info: {
236
+ title: config.title,
237
+ version: config.version,
238
+ ...config.description ? { description: config.description } : {}
239
+ },
240
+ servers: [{ url: config.serverUrl ?? "/" }],
241
+ paths,
242
+ components: { schemas },
243
+ ...tags.length > 0 ? { tags } : {}
244
+ };
245
+ }
246
+ function openAPIHandler(config) {
247
+ const spec = generateOpenAPISpec(config);
248
+ const json = JSON.stringify(spec, null, 2);
249
+ return () => new Response(json, {
250
+ headers: {
251
+ "Content-Type": "application/json",
252
+ "Access-Control-Allow-Origin": "*"
253
+ }
254
+ });
255
+ }
256
+ // Annotate the CommonJS export names for ESM import in node:
257
+ 0 && (module.exports = {
258
+ generateOpenAPISpec,
259
+ openAPIHandler
260
+ });
@@ -0,0 +1,97 @@
1
+ import {
2
+ zodToJsonSchema
3
+ } from "../chunk-OH73K7I5.mjs";
4
+ import "../chunk-Y6FXYEAI.mjs";
5
+
6
+ // src/openapi/index.ts
7
+ function generateOpenAPISpec(config) {
8
+ const basePath = config.basePath ?? "/api/tools";
9
+ const paths = {};
10
+ const schemas = {};
11
+ const autoTags = /* @__PURE__ */ new Set();
12
+ for (const [key, tool] of Object.entries(config.tools)) {
13
+ const name = tool.name || key;
14
+ const inputSchemaName = `${name}Input`;
15
+ const outputSchemaName = `${name}Output`;
16
+ schemas[inputSchemaName] = zodToJsonSchema(tool.inputSchema);
17
+ if (tool.outputSchema) {
18
+ schemas[outputSchemaName] = zodToJsonSchema(tool.outputSchema);
19
+ }
20
+ const toolTags = tool.tags.length > 0 ? tool.tags : [name];
21
+ for (const t of toolTags) autoTags.add(t);
22
+ const responseSchema = tool.outputSchema ? { $ref: `#/components/schemas/${outputSchemaName}` } : { type: "object" };
23
+ const hints = [];
24
+ if (tool.hints.destructive) hints.push("destructive");
25
+ if (tool.hints.readOnly) hints.push("read-only");
26
+ if (tool.hints.idempotent) hints.push("idempotent");
27
+ if (tool.requiresConfirmation) hints.push("requires confirmation");
28
+ const hintsStr = hints.length > 0 ? ` [${hints.join(", ")}]` : "";
29
+ paths[`${basePath}/${name}`] = {
30
+ post: {
31
+ operationId: name,
32
+ summary: tool.description || name,
33
+ description: (tool.description || "") + hintsStr,
34
+ tags: tool.tags.length > 0 ? tool.tags : void 0,
35
+ requestBody: {
36
+ required: true,
37
+ content: {
38
+ "application/json": {
39
+ schema: { $ref: `#/components/schemas/${inputSchemaName}` }
40
+ }
41
+ }
42
+ },
43
+ responses: {
44
+ "200": {
45
+ description: "Successful tool execution",
46
+ content: {
47
+ "application/json": {
48
+ schema: {
49
+ type: "object",
50
+ properties: {
51
+ result: responseSchema
52
+ }
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "400": {
58
+ description: "Invalid input (Zod validation error)"
59
+ },
60
+ "404": {
61
+ description: "Tool not found"
62
+ },
63
+ "500": {
64
+ description: "Tool execution failed"
65
+ }
66
+ }
67
+ }
68
+ };
69
+ }
70
+ const tags = config.tags ?? [...autoTags].map((t) => ({ name: t }));
71
+ return {
72
+ openapi: "3.1.0",
73
+ info: {
74
+ title: config.title,
75
+ version: config.version,
76
+ ...config.description ? { description: config.description } : {}
77
+ },
78
+ servers: [{ url: config.serverUrl ?? "/" }],
79
+ paths,
80
+ components: { schemas },
81
+ ...tags.length > 0 ? { tags } : {}
82
+ };
83
+ }
84
+ function openAPIHandler(config) {
85
+ const spec = generateOpenAPISpec(config);
86
+ const json = JSON.stringify(spec, null, 2);
87
+ return () => new Response(json, {
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ "Access-Control-Allow-Origin": "*"
91
+ }
92
+ });
93
+ }
94
+ export {
95
+ generateOpenAPISpec,
96
+ openAPIHandler
97
+ };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Lightweight Zod-to-JSON-Schema converter.
5
+ * Handles common Zod types without requiring zod-to-json-schema dependency.
6
+ */
7
+
8
+ interface JsonSchema {
9
+ type?: string;
10
+ properties?: Record<string, JsonSchema>;
11
+ required?: string[];
12
+ items?: JsonSchema;
13
+ enum?: unknown[];
14
+ description?: string;
15
+ default?: unknown;
16
+ minimum?: number;
17
+ maximum?: number;
18
+ minLength?: number;
19
+ maxLength?: number;
20
+ pattern?: string;
21
+ format?: string;
22
+ anyOf?: JsonSchema[];
23
+ oneOf?: JsonSchema[];
24
+ nullable?: boolean;
25
+ additionalProperties?: boolean | JsonSchema;
26
+ [key: string]: unknown;
27
+ }
28
+ declare function zodToJsonSchema(schema: z.ZodType | {
29
+ [key: string]: any;
30
+ }): JsonSchema;
31
+
32
+ export { type JsonSchema as J, zodToJsonSchema as z };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Lightweight Zod-to-JSON-Schema converter.
5
+ * Handles common Zod types without requiring zod-to-json-schema dependency.
6
+ */
7
+
8
+ interface JsonSchema {
9
+ type?: string;
10
+ properties?: Record<string, JsonSchema>;
11
+ required?: string[];
12
+ items?: JsonSchema;
13
+ enum?: unknown[];
14
+ description?: string;
15
+ default?: unknown;
16
+ minimum?: number;
17
+ maximum?: number;
18
+ minLength?: number;
19
+ maxLength?: number;
20
+ pattern?: string;
21
+ format?: string;
22
+ anyOf?: JsonSchema[];
23
+ oneOf?: JsonSchema[];
24
+ nullable?: boolean;
25
+ additionalProperties?: boolean | JsonSchema;
26
+ [key: string]: unknown;
27
+ }
28
+ declare function zodToJsonSchema(schema: z.ZodType | {
29
+ [key: string]: any;
30
+ }): JsonSchema;
31
+
32
+ export { type JsonSchema as J, zodToJsonSchema as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lantos1618/better-ui",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
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",
@@ -41,6 +41,11 @@
41
41
  "import": "./dist/agui/index.mjs",
42
42
  "require": "./dist/agui/index.js"
43
43
  },
44
+ "./openapi": {
45
+ "types": "./dist/openapi/index.d.ts",
46
+ "import": "./dist/openapi/index.mjs",
47
+ "require": "./dist/openapi/index.js"
48
+ },
44
49
  "./theme.css": "./src/theme.css"
45
50
  },
46
51
  "files": [
@@ -69,7 +74,7 @@
69
74
  },
70
75
  "scripts": {
71
76
  "build": "npm run build:lib",
72
- "build:lib": "tsup src/index.ts src/react/index.ts src/components/index.ts src/auth/index.ts src/persistence/index.ts src/mcp/index.ts src/agui/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.lib.json",
77
+ "build:lib": "tsup src/index.ts src/react/index.ts src/components/index.ts src/auth/index.ts src/persistence/index.ts src/mcp/index.ts src/agui/index.ts src/openapi/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.lib.json",
73
78
  "test": "jest",
74
79
  "type-check": "tsc --noEmit",
75
80
  "prepublishOnly": "npm run build:lib"