@lantos1618/better-ui 0.4.1 → 0.6.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/README.md CHANGED
@@ -1,29 +1,18 @@
1
1
  # Better UI
2
2
 
3
- > A minimal, type-safe AI-first UI framework for building tools
3
+ > Define once. Render in UI. Serve over MCP. Type-safe AI tools with views.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@lantos1618/better-ui.svg)](https://www.npmjs.com/package/@lantos1618/better-ui)
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/)
8
8
 
9
- ## What is Better UI?
9
+ ## The Problem
10
10
 
11
- Better UI provides a clean, fluent API for creating tools that AI assistants can execute. Define input/output schemas with Zod, implement server and client logic separately, and render results with React components.
11
+ Every AI framework lets you define tools. None of them let the tool own its own UI. You end up with tool definitions in one place, rendering logic scattered somewhere else, and no way to expose those same tools to external AI clients.
12
12
 
13
- **Key differentiators**:
14
- - **View integration** - tools render their own results in UI (no other framework does this)
15
- - **Multi-provider support** - OpenAI, Anthropic, Google Gemini, OpenRouter
16
- - **Streaming tool views** - progressive partial data rendering
17
- - **Pre-made chat components** - drop-in `<Chat />` with automatic tool view rendering
18
- - **Server infrastructure** - rate limiting, caching, Next.js/Express adapters
13
+ ## The Solution
19
14
 
20
- ## Installation
21
-
22
- ```bash
23
- npm install @lantos1618/better-ui zod
24
- ```
25
-
26
- ## Quick Start
15
+ Better UI tools are self-contained units: **schema + server logic + view + streaming**, all in one definition. Use them in chat, call them from React, or expose them as an MCP server — same tool, zero glue code.
27
16
 
28
17
  ```typescript
29
18
  import { tool } from '@lantos1618/better-ui';
@@ -36,82 +25,97 @@ const weather = tool({
36
25
  output: z.object({ temp: z.number(), condition: z.string() }),
37
26
  });
38
27
 
39
- // Server implementation (runs on server)
40
28
  weather.server(async ({ city }) => {
41
29
  const data = await weatherAPI.get(city);
42
30
  return { temp: data.temp, condition: data.condition };
43
31
  });
44
32
 
45
- // View for rendering results (our differentiator!)
46
33
  weather.view((data) => (
47
34
  <div className="weather-card">
48
- <span>{data.temp}</span>
35
+ <span>{data.temp}°</span>
49
36
  <span>{data.condition}</span>
50
37
  </div>
51
38
  ));
52
39
  ```
53
40
 
54
- ## Drop-in Chat UI
41
+ That's it. The tool validates input/output with Zod, runs server logic securely, and renders its own results. Drop it into chat and it just works. Expose it over MCP and Claude Desktop can call it.
55
42
 
56
- ```tsx
57
- import { Chat } from '@lantos1618/better-ui/components';
43
+ ## Install
58
44
 
59
- function App() {
60
- return (
61
- <Chat
62
- endpoint="/api/chat"
63
- tools={{ weather, search, counter }}
64
- className="h-[600px]"
65
- placeholder="Ask something..."
66
- />
67
- );
68
- }
45
+ ```bash
46
+ npm install @lantos1618/better-ui zod
69
47
  ```
70
48
 
71
- Or compose your own layout:
49
+ ## What You Get
72
50
 
73
- ```tsx
74
- import { ChatProvider, Thread, Composer } from '@lantos1618/better-ui/components';
75
-
76
- function App() {
77
- return (
78
- <ChatProvider endpoint="/api/chat" tools={tools}>
79
- <div className="flex flex-col h-screen">
80
- <Thread className="flex-1 overflow-y-auto" />
81
- <Composer placeholder="Type a message..." />
82
- </div>
83
- </ChatProvider>
84
- );
85
- }
86
- ```
51
+ | Feature | What |
52
+ |---------|------|
53
+ | **View integration** | Tools render their own results — no other framework does this |
54
+ | **MCP server** | Expose any tool registry to Claude Desktop, Cursor, VS Code |
55
+ | **AG-UI protocol** | Compatible with CopilotKit, LangChain, Google ADK frontends |
56
+ | **Multi-provider** | OpenAI, Anthropic, Google Gemini, OpenRouter |
57
+ | **Streaming views** | Progressive partial data rendering |
58
+ | **Drop-in chat** | `<Chat />` component with automatic tool view rendering |
59
+ | **HITL confirmation** | Tools can require human approval before executing |
60
+ | **Auth helpers** | JWT, session cookies, BetterAuth integration |
61
+ | **Security** | Context stripping, input validation, output sanitization, rate limiting |
87
62
 
88
- Tool results in chat automatically render using the tool's `.view()` component.
63
+ ## Quick Start
89
64
 
90
- ## Multi-Provider Support
65
+ ### 1. Define a Tool
91
66
 
92
67
  ```typescript
93
- import { createProvider } from '@lantos1618/better-ui';
68
+ import { tool } from '@lantos1618/better-ui';
69
+ import { z } from 'zod';
94
70
 
95
- // OpenAI
96
- const provider = createProvider({ provider: 'openai', model: 'gpt-4o' });
71
+ export const search = tool({
72
+ name: 'search',
73
+ description: 'Search the web',
74
+ input: z.object({ query: z.string().max(1000) }),
75
+ output: z.object({
76
+ results: z.array(z.object({
77
+ title: z.string(),
78
+ url: z.string(),
79
+ })),
80
+ }),
81
+ });
97
82
 
98
- // Anthropic
99
- const provider = createProvider({ provider: 'anthropic', model: 'claude-4-sonnet' });
83
+ search.server(async ({ query }) => {
84
+ const results = await searchAPI.search(query);
85
+ return { results };
86
+ });
100
87
 
101
- // Google Gemini
102
- const provider = createProvider({ provider: 'google', model: 'gemini-2.5-pro' });
88
+ search.view((data) => (
89
+ <ul>
90
+ {data.results.map((r, i) => (
91
+ <li key={i}><a href={r.url}>{r.title}</a></li>
92
+ ))}
93
+ </ul>
94
+ ));
95
+ ```
103
96
 
104
- // OpenRouter (access any model)
105
- const provider = createProvider({
106
- provider: 'openrouter',
107
- model: 'anthropic/claude-4-sonnet',
108
- apiKey: process.env.OPENROUTER_API_KEY,
109
- });
97
+ ### 2. Use It in Chat
98
+
99
+ ```tsx
100
+ import { Chat } from '@lantos1618/better-ui/components';
101
+
102
+ function App() {
103
+ return (
104
+ <Chat
105
+ endpoint="/api/chat"
106
+ tools={{ weather, search }}
107
+ className="h-[600px]"
108
+ />
109
+ );
110
+ }
110
111
  ```
111
112
 
112
- Use with the chat handler:
113
+ Tool results render automatically using the tool's `.view()` component.
114
+
115
+ ### 3. Wire Up the API Route
113
116
 
114
117
  ```typescript
118
+ // app/api/chat/route.ts (Next.js)
115
119
  import { streamText, convertToModelMessages } from 'ai';
116
120
  import { createProvider } from '@lantos1618/better-ui';
117
121
 
@@ -124,249 +128,448 @@ export async function POST(req: Request) {
124
128
  messages: convertToModelMessages(messages),
125
129
  tools: {
126
130
  weather: weatherTool.toAITool(),
131
+ search: searchTool.toAITool(),
127
132
  },
128
133
  });
129
134
  return result.toUIMessageStreamResponse();
130
135
  }
131
136
  ```
132
137
 
133
- ## Streaming Tool Views
134
-
135
- Tools can stream partial results progressively:
138
+ ### 4. Or Expose as an MCP Server
136
139
 
137
140
  ```typescript
138
- import { useToolStream } from '@lantos1618/better-ui';
141
+ import { createMCPServer } from '@lantos1618/better-ui/mcp';
139
142
 
140
- // Define a streaming tool
141
- const analysis = tool({
142
- name: 'analysis',
143
- input: z.object({ query: z.string() }),
144
- output: z.object({ summary: z.string(), score: z.number() }),
143
+ const server = createMCPServer({
144
+ name: 'my-tools',
145
+ version: '1.0.0',
146
+ tools: { weather, search },
145
147
  });
146
148
 
147
- analysis.stream(async ({ query }, { stream }) => {
148
- stream({ summary: 'Analyzing...' }); // Partial update
149
- const result = await analyzeData(query);
150
- stream({ summary: result.summary }); // More data
151
- return { summary: result.summary, score: result.score }; // Final
152
- });
149
+ server.start(); // stdio transport works with Claude Desktop
150
+ ```
153
151
 
154
- // In a component:
155
- function AnalysisWidget({ query }) {
156
- const { data, streaming, loading, execute } = useToolStream(analysis);
152
+ Add to Claude Desktop config (`~/.claude/claude_desktop_config.json`):
157
153
 
158
- return (
159
- <div>
160
- <button onClick={() => execute({ query })}>Analyze</button>
161
- {loading && <p>Starting...</p>}
162
- {streaming && <p>Streaming: {data?.summary}</p>}
163
- {data?.score && <p>Score: {data.score}</p>}
164
- </div>
165
- );
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "my-tools": {
158
+ "command": "npx",
159
+ "args": ["tsx", "path/to/mcp-server.ts"]
160
+ }
161
+ }
166
162
  }
167
163
  ```
168
164
 
169
- ## Core Concepts
165
+ Or use the HTTP handler for web-based MCP clients:
166
+
167
+ ```typescript
168
+ // app/api/mcp/route.ts
169
+ export const POST = server.httpHandler();
170
+ ```
170
171
 
171
- ### 1. Tool Definition
172
+ ---
173
+
174
+ ## Tool API
175
+
176
+ ### Object Config
172
177
 
173
178
  ```typescript
174
179
  const myTool = tool({
175
180
  name: 'myTool',
176
181
  description: 'What this tool does',
177
- input: z.object({ /* input schema */ }),
178
- output: z.object({ /* output schema */ }),
179
- tags: ['category', 'type'],
180
- cache: { ttl: 60000 }, // optional caching
182
+ input: z.object({ query: z.string() }),
183
+ output: z.object({ results: z.array(z.string()) }),
184
+ tags: ['search'],
185
+ cache: { ttl: 60000 },
186
+ confirm: true, // require HITL confirmation
187
+ hints: { destructive: true }, // behavioral metadata
188
+ autoRespond: true, // auto-send state back to AI after user action
189
+ groupKey: (input) => input.query, // collapse related calls in thread
181
190
  });
182
191
  ```
183
192
 
184
- ### 2. Server Implementation
185
-
186
- The `.server()` method defines logic that runs on the server (API routes, server components):
193
+ ### Fluent Builder
187
194
 
188
195
  ```typescript
189
- myTool.server(async ({ query }, ctx) => {
190
- // Direct access to databases, secrets, file system
191
- const results = await db.search(query);
192
- return { results };
193
- });
196
+ const search = tool('search')
197
+ .description('Search the database')
198
+ .input(z.object({ query: z.string() }))
199
+ .output(z.object({ results: z.array(z.string()) }))
200
+ .server(async ({ query }) => ({ results: await db.search(query) }))
201
+ .view((data) => <ResultsList items={data.results} />)
202
+ .build();
194
203
  ```
195
204
 
196
- ### 3. Client Implementation (Optional)
197
-
198
- The `.client()` method defines what happens when called from the browser. If not specified, auto-fetches to `/api/tools/execute`.
205
+ ### Handlers
199
206
 
200
207
  ```typescript
201
- myTool.client(async ({ query }, ctx) => {
202
- const cached = ctx.cache.get(query);
203
- if (cached) return cached;
204
-
205
- return ctx.fetch('/api/search', {
206
- method: 'POST',
207
- body: JSON.stringify({ query })
208
- });
208
+ // Server runs in API routes, never on client
209
+ myTool.server(async (input, ctx) => {
210
+ // ctx.env, ctx.headers, ctx.cookies, ctx.user, ctx.session
211
+ return await db.query(input.query);
209
212
  });
210
- ```
211
213
 
212
- ### 4. View (Our Differentiator)
214
+ // Client — runs in browser. Auto-fetches to /api/tools/execute if not defined
215
+ myTool.client(async (input, ctx) => {
216
+ return ctx.fetch('/api/search', { method: 'POST', body: JSON.stringify(input) });
217
+ });
213
218
 
214
- The `.view()` method defines how to render the tool's results:
219
+ // Stream progressive partial updates
220
+ myTool.stream(async (input, { stream }) => {
221
+ stream({ status: 'searching...' });
222
+ const results = await search(input.query);
223
+ stream({ results, status: 'done' });
224
+ return { results, status: 'done', count: results.length };
225
+ });
215
226
 
216
- ```typescript
217
- myTool.view((data, { loading, error, streaming }) => {
227
+ // View — render results (the differentiator)
228
+ myTool.view((data, { loading, error, streaming, onAction }) => {
218
229
  if (loading) return <Spinner />;
219
- if (error) return <Error message={error.message} />;
230
+ if (error) return <ErrorCard message={error.message} />;
220
231
  if (streaming) return <PartialResults data={data} />;
221
232
  return <Results items={data.results} />;
222
233
  });
223
234
  ```
224
235
 
225
- ### 5. Streaming
226
-
227
- The `.stream()` method enables progressive partial updates:
236
+ ### Execution
228
237
 
229
238
  ```typescript
230
- myTool.stream(async ({ query }, { stream }) => {
231
- stream({ status: 'searching...' });
232
- const results = await search(query);
233
- stream({ results, status: 'done' });
234
- return { results, status: 'done', count: results.length };
235
- });
236
- ```
239
+ // Server-side
240
+ const result = await myTool.run(input, { isServer: true });
237
241
 
238
- ## Fluent Builder Alternative
242
+ // Client-side (auto-fetches if no .client() defined)
243
+ const result = await myTool.run(input, { isServer: false });
239
244
 
240
- ```typescript
241
- const search = tool('search')
242
- .description('Search the database')
243
- .input(z.object({ query: z.string() }))
244
- .output(z.object({ results: z.array(z.string()) }))
245
- .server(async ({ query }) => ({ results: await db.search(query) }))
246
- .stream(async ({ query }, { stream }) => {
247
- stream({ results: [] });
248
- const results = await db.search(query);
249
- return { results };
250
- })
251
- .view((data) => <ResultsList items={data.results} />);
245
+ // Streaming
246
+ for await (const { partial, done } of myTool.runStream(input)) {
247
+ console.log(partial); // progressive updates
248
+ if (done) break;
249
+ }
250
+
251
+ // AI SDK integration
252
+ const aiTool = myTool.toAITool(); // { description, inputSchema, execute }
252
253
  ```
253
254
 
255
+ ---
256
+
254
257
  ## React Hooks
255
258
 
256
- ### `useTool(tool, input?, options?)`
259
+ ```typescript
260
+ import { useTool, useTools, useToolStream } from '@lantos1618/better-ui/react';
261
+ ```
262
+
263
+ ### `useTool`
257
264
 
258
265
  ```typescript
259
- import { useTool } from '@lantos1618/better-ui';
260
-
261
- const {
262
- data, // Result data
263
- loading, // Loading state
264
- error, // Error if any
265
- execute, // Execute function
266
- reset, // Reset state
267
- executed, // Has been executed
268
- } = useTool(myTool, initialInput, {
266
+ const { data, loading, error, execute, reset, executed } = useTool(myTool, initialInput, {
269
267
  auto: false,
270
268
  onSuccess: (data) => {},
271
269
  onError: (error) => {},
272
270
  });
273
271
  ```
274
272
 
275
- ### `useToolStream(tool, options?)`
273
+ ### `useToolStream`
276
274
 
277
275
  ```typescript
278
- import { useToolStream } from '@lantos1618/better-ui';
279
-
280
- const {
281
- data, // Progressive partial data
282
- finalData, // Complete validated data (when done)
283
- streaming, // True while receiving partial updates
284
- loading, // True before first chunk
285
- error,
286
- execute,
287
- reset,
288
- } = useToolStream(myTool);
276
+ const { data, finalData, streaming, loading, error, execute, reset } = useToolStream(myTool);
289
277
  ```
290
278
 
291
- ### `useTools(tools, options?)`
279
+ ### `useTools`
292
280
 
293
281
  ```typescript
294
- import { useTools } from '@lantos1618/better-ui';
295
-
296
282
  const tools = useTools({ weather, search });
297
283
 
298
284
  await tools.weather.execute({ city: 'London' });
299
- await tools.search.execute({ query: 'restaurants' });
300
-
301
- tools.weather.data; // Weather result
302
- tools.search.loading; // Search loading state
285
+ tools.weather.data; // result
286
+ tools.search.loading; // loading state
303
287
  ```
304
288
 
289
+ ---
290
+
305
291
  ## Chat Components
306
292
 
307
- Import from `@lantos1618/better-ui/components`:
293
+ ```typescript
294
+ import { Chat, ChatProvider, Thread, Composer, Message, ToolResult } from '@lantos1618/better-ui/components';
295
+ ```
296
+
297
+ ### Drop-in
298
+
299
+ ```tsx
300
+ <Chat endpoint="/api/chat" tools={{ weather, search }} className="h-[600px]" />
301
+ ```
302
+
303
+ ### Composable
304
+
305
+ ```tsx
306
+ <ChatProvider endpoint="/api/chat" tools={tools}>
307
+ <div className="flex flex-col h-screen">
308
+ <Thread className="flex-1 overflow-y-auto" />
309
+ <Composer placeholder="Type a message..." />
310
+ </div>
311
+ </ChatProvider>
312
+ ```
313
+
314
+ ### All Components
308
315
 
309
316
  | Component | Description |
310
317
  |-----------|-------------|
311
- | `Chat` | All-in-one chat component (ChatProvider + Thread + Composer) |
318
+ | `Chat` | All-in-one (ChatProvider + Thread + Composer) |
312
319
  | `ChatProvider` | Context provider wrapping AI SDK's `useChat` |
313
320
  | `Thread` | Message list with auto-scroll |
314
- | `Message` | Single message with automatic tool view rendering |
321
+ | `Message` | Single message with tool view rendering |
315
322
  | `Composer` | Input form with send button |
316
- | `ToolResult` | Renders a tool's `.View` in chat context |
323
+ | `ToolResult` | Renders a tool's `.view()` in chat context |
324
+ | `Panel` / `ChatPanel` | Sidebar panel for thread management |
325
+ | `Markdown` | Markdown renderer with syntax highlighting |
326
+ | `ThemeProvider` | Theme CSS variable provider |
327
+
328
+ ### View Building Blocks
329
+
330
+ Pre-made view components for common patterns:
331
+
332
+ ```typescript
333
+ import {
334
+ QuestionView, // Multiple choice / free-text questions
335
+ FormView, // Dynamic forms
336
+ DataTableView, // Sortable data tables
337
+ ProgressView, // Step-by-step progress
338
+ MediaDisplayView,// Image/video display
339
+ CodeBlockView, // Syntax-highlighted code
340
+ FileUploadView, // File upload UI
341
+ } from '@lantos1618/better-ui/components';
342
+ ```
343
+
344
+ ---
345
+
346
+ ## MCP Server
347
+
348
+ Turn any tool registry into an [MCP](https://modelcontextprotocol.io) server. Zero dependencies beyond Better UI itself.
349
+
350
+ ```typescript
351
+ import { createMCPServer } from '@lantos1618/better-ui/mcp';
352
+
353
+ const server = createMCPServer({
354
+ name: 'my-app',
355
+ version: '1.0.0',
356
+ tools: { weather, search, calculator },
357
+ context: { env: process.env }, // passed to every tool execution
358
+ });
359
+ ```
360
+
361
+ ### Transports
362
+
363
+ ```typescript
364
+ // stdio — for Claude Desktop, Cursor, VS Code extensions
365
+ server.start();
366
+
367
+ // HTTP — for Next.js, Express, Cloudflare Workers, Deno
368
+ const handler = server.httpHandler();
369
+ // Use as: export const POST = handler;
370
+ ```
371
+
372
+ ### Programmatic Use
373
+
374
+ ```typescript
375
+ // List tools with JSON schemas
376
+ const tools = server.listTools();
377
+
378
+ // Call a tool directly
379
+ const result = await server.callTool('weather', { city: 'Tokyo' });
380
+ // → { content: [{ type: 'text', text: '{"temp":21,"city":"Tokyo","condition":"sunny"}' }] }
381
+
382
+ // Handle raw JSON-RPC messages
383
+ const response = await server.handleMessage({
384
+ jsonrpc: '2.0',
385
+ id: 1,
386
+ method: 'tools/call',
387
+ params: { name: 'weather', arguments: { city: 'Tokyo' } },
388
+ });
389
+ ```
390
+
391
+ ### Schema Conversion
392
+
393
+ The built-in `zodToJsonSchema` converter handles common Zod types without extra dependencies:
317
394
 
318
- All components accept `className` for styling customization.
395
+ ```typescript
396
+ import { zodToJsonSchema } from '@lantos1618/better-ui/mcp';
397
+
398
+ zodToJsonSchema(z.object({
399
+ name: z.string().min(1).max(100),
400
+ age: z.number().int().min(0),
401
+ role: z.enum(['admin', 'user']),
402
+ }));
403
+ // → { type: 'object', properties: { name: { type: 'string', ... }, ... }, required: [...] }
404
+ ```
405
+
406
+ ---
407
+
408
+ ## AG-UI Protocol
409
+
410
+ Expose your tools via the [AG-UI (Agent-User Interaction Protocol)](https://docs.ag-ui.com) — compatible with CopilotKit, LangChain, Google ADK, and any AG-UI frontend.
411
+
412
+ ```typescript
413
+ import { createAGUIServer } from '@lantos1618/better-ui/agui';
414
+
415
+ const server = createAGUIServer({
416
+ name: 'my-tools',
417
+ tools: { weather, search },
418
+ });
419
+
420
+ // Next.js route handler — returns SSE event stream
421
+ export const POST = server.handler();
422
+ ```
423
+
424
+ The handler emits standard AG-UI events (`RUN_STARTED`, `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_RESULT`, `TOOL_CALL_END`, `RUN_FINISHED`) over Server-Sent Events.
425
+
426
+ ---
319
427
 
320
428
  ## Providers
321
429
 
322
- | Provider | Package Required | Example Model |
323
- |----------|-----------------|---------------|
430
+ ```typescript
431
+ import { createProvider } from '@lantos1618/better-ui';
432
+
433
+ createProvider({ provider: 'openai', model: 'gpt-4o' });
434
+ createProvider({ provider: 'anthropic', model: 'claude-4-sonnet' });
435
+ createProvider({ provider: 'google', model: 'gemini-2.5-pro' });
436
+ createProvider({ provider: 'openrouter', model: 'anthropic/claude-4-sonnet', apiKey: '...' });
437
+ ```
438
+
439
+ | Provider | Package | Example Models |
440
+ |----------|---------|----------------|
324
441
  | OpenAI | `@ai-sdk/openai` (included) | `gpt-4o`, `gpt-5.2` |
325
- | Anthropic | `@ai-sdk/anthropic` (optional) | `claude-4-sonnet` |
442
+ | Anthropic | `@ai-sdk/anthropic` (optional) | `claude-4-sonnet`, `claude-4-opus` |
326
443
  | Google | `@ai-sdk/google` (optional) | `gemini-2.5-pro` |
327
- | OpenRouter | `@ai-sdk/openai` (included) | `anthropic/claude-4-sonnet` |
444
+ | OpenRouter | `@ai-sdk/openai` (included) | any model via `provider/model` |
328
445
 
329
- ```bash
330
- # Optional providers
331
- npm install @ai-sdk/anthropic # For Anthropic
332
- npm install @ai-sdk/google # For Google Gemini
446
+ ---
447
+
448
+ ## Auth
449
+
450
+ ```typescript
451
+ import { jwtAuth, sessionAuth, betterAuth } from '@lantos1618/better-ui/auth';
452
+
453
+ // JWT Bearer tokens
454
+ const auth = jwtAuth({ secret: process.env.JWT_SECRET!, issuer: 'my-app' });
455
+
456
+ // Cookie-based sessions
457
+ const auth = sessionAuth({ cookieName: 'session', verify: async (token) => db.getSession(token) });
458
+
459
+ // BetterAuth integration
460
+ const auth = betterAuth(authInstance);
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Persistence
466
+
467
+ ```typescript
468
+ import { createMemoryAdapter } from '@lantos1618/better-ui/persistence';
469
+
470
+ const adapter = createMemoryAdapter(); // in-memory, for dev/testing
471
+
472
+ await adapter.createThread('New Chat');
473
+ await adapter.saveMessages(threadId, messages);
474
+ await adapter.getMessages(threadId);
333
475
  ```
334
476
 
477
+ Implement the `PersistenceAdapter` interface for Drizzle, Prisma, or any database.
478
+
479
+ ---
480
+
481
+ ## HITL (Human-in-the-Loop)
482
+
483
+ Tools can require confirmation before executing:
484
+
485
+ ```typescript
486
+ const sendEmail = tool({
487
+ name: 'sendEmail',
488
+ description: 'Send an email',
489
+ input: z.object({ to: z.string().email(), subject: z.string(), body: z.string() }),
490
+ confirm: true, // always require confirmation
491
+ // or: confirm: (input) => input.to.endsWith('@company.com') // conditional
492
+ // or: hints: { destructive: true } // auto-implies confirmation
493
+ });
494
+ ```
495
+
496
+ When `confirm` is set, `toAITool()` omits the `execute` function, leaving the tool call at `state: 'input-available'` for client-side confirmation before execution.
497
+
498
+ ---
499
+
500
+ ## Security
501
+
502
+ Better UI is designed with security boundaries between server and client:
503
+
504
+ - **Context stripping** — `env`, `headers`, `cookies`, `user`, `session` are automatically removed when running on the client
505
+ - **Input validation** — Zod schemas validate and strip unknown keys before execution
506
+ - **Output validation** — Output schemas prevent accidental data leakage (extra fields are stripped)
507
+ - **Server isolation** — Server handlers never run on the client; auto-fetch kicks in instead
508
+ - **Serialization safety** — `toJSON()` excludes handlers, schemas, and internal config
509
+ - **Rate limiting** — Pluggable rate limiter with in-memory and Redis backends
510
+ - **Audit logging** — Structured JSON logging for every tool execution
511
+ - **Prototype pollution protection** — Safe object merging in state context handling
512
+ - **MCP hardening** — `hasOwnProperty` checks prevent prototype chain traversal on tool lookup
513
+
514
+ ---
515
+
335
516
  ## Project Structure
336
517
 
337
518
  ```
338
519
  src/
339
- tool.tsx # Core tool() API with streaming
520
+ tool.tsx Core tool() API schema, handlers, view, streaming
521
+ index.ts Main exports (server-safe, no React)
340
522
  react/
341
- useTool.ts # React hooks (useTool, useTools)
342
- useToolStream.ts # Streaming hook
523
+ useTool.ts useTool, useTools hooks
524
+ useToolStream.ts useToolStream hook
343
525
  components/
344
- Chat.tsx # All-in-one chat component
345
- ChatProvider.tsx # Chat context provider
346
- Thread.tsx # Message list
347
- Message.tsx # Single message
348
- Composer.tsx # Input form
349
- ToolResult.tsx # Tool view renderer
526
+ Chat.tsx All-in-one chat
527
+ ChatProvider.tsx Chat context provider
528
+ Thread.tsx Message list
529
+ Message.tsx Single message
530
+ Composer.tsx Input form
531
+ ToolResult.tsx Tool view renderer
532
+ Panel.tsx Sidebar panel
533
+ Markdown.tsx Markdown renderer
534
+ Question.tsx Question view block
535
+ Form.tsx Form view block
536
+ DataTable.tsx Data table view block
537
+ Progress.tsx Progress view block
538
+ MediaDisplay.tsx Media view block
539
+ CodeBlock.tsx Code block view block
540
+ FileUpload.tsx File upload view block
541
+ Toast.tsx Toast notifications
542
+ ThemeProvider.tsx Theme CSS variables
350
543
  providers/
351
- openai.ts # OpenAI adapter
352
- anthropic.ts # Anthropic adapter
353
- google.ts # Google Gemini adapter
354
- openrouter.ts # OpenRouter adapter
355
- adapters/
356
- nextjs.ts # Next.js route handlers
357
- express.ts # Express middleware
358
- index.ts # Main exports
544
+ openai.ts OpenAI adapter
545
+ anthropic.ts Anthropic adapter
546
+ google.ts Google Gemini adapter
547
+ openrouter.ts OpenRouter adapter
548
+ auth/
549
+ jwt.ts JWT auth helper
550
+ session.ts Session cookie auth
551
+ better-auth.ts BetterAuth integration
552
+ persistence/
553
+ types.ts PersistenceAdapter interface
554
+ memory.ts In-memory adapter
555
+ mcp/
556
+ server.ts MCP server (stdio + HTTP + SSE)
557
+ schema.ts Zod → JSON Schema converter
558
+ agui/
559
+ server.ts AG-UI protocol server (SSE)
560
+ examples/
561
+ nextjs-demo/ Full Next.js demo app
562
+ vite-demo/ Vite + Express demo app
563
+ mcp-server/ Standalone MCP server example
359
564
  ```
360
565
 
361
566
  ## Development
362
567
 
363
568
  ```bash
364
569
  npm install
365
- npm run dev # Run dev server
366
- npm run build:lib # Build library
367
- npm run build # Build everything
570
+ npm run build # Build library
571
+ npm test # Run tests (226 tests)
368
572
  npm run type-check # TypeScript check
369
- npm test # Run tests
370
573
  ```
371
574
 
372
575
  ## License