weifuwu 0.31.1 → 0.32.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
@@ -21,7 +21,7 @@ serve(app, { port: 3000 })
21
21
  | Export | Description |
22
22
  |---|---|
23
23
  | `serve(app, opts?)` | Start HTTP server. Returns `Server` with `port`, `hostname`, `ready`, `close()`. |
24
- | `Router` | Trie-based HTTP router with WebSocket support. |
24
+ | `Router` | Trie-based HTTP router with WebSocket support and `plugin()` method. |
25
25
  | `HttpError` | `new HttpError(message, status)`. Throw to return that status code. |
26
26
  | `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB). |
27
27
 
@@ -42,6 +42,7 @@ app.ws(path, ...middlewares, handler)
42
42
  // Middleware & mounting
43
43
  app.use(middleware) // global middleware
44
44
  app.mount(prefix, router) // sub-router at prefix
45
+ app.plugin(fn) // extension: (app) => { app.get(), app.use(), ... }
45
46
  app.onError(handler) // error handler: (error, req, ctx) => Response
46
47
  app.routes() // debug: list all registered routes
47
48
 
@@ -63,7 +64,8 @@ app.get('/users/:id', (req, ctx) => {
63
64
  | `logger(opts?)` | Request logging. `format: 'short' | 'combined' | 'json'`. |
64
65
  | `upload(opts?)` | Multipart file upload via `req.formData()`. Injects `ctx.parsed`. |
65
66
  | `serveStatic(root, opts?)` | Static file handler. `cacheControl`, `index`. |
66
- | `trace(opts?)` | Injects `ctx.trace = { requestId, traceId, elapsed(), startTime }`. |
67
+ | `sandbox(opts?)` | Filesystem isolation for agent operations. `baseDir`, `timeout`, `isolateBy`. |
68
+ | `auth(opts?)` | Authentication: JWT, session cookies, API keys. Injects `ctx.user`. |
67
69
 
68
70
  ### Tracing
69
71
 
@@ -178,111 +180,241 @@ npm install react react-dom
178
180
  ```
179
181
 
180
182
  ```ts
181
- import { react, Link, useServerData, ErrorBoundary } from 'weifuwu'
182
- import { createElement as h } from 'react'
183
+ // server.ts the only file you need
184
+ import { serve, Router } from 'weifuwu'
185
+ import { react } from 'weifuwu/react'
183
186
 
184
- // ── Server ──────────────────────────────────────────────
187
+ const app = new Router()
188
+ .use(trace())
189
+ .use(logger())
190
+ .plugin(react({
191
+ pages: {
192
+ '/': './pages/Home.tsx',
193
+ '/users': './pages/Users.tsx',
194
+ '/users/:id': './pages/UserDetail.tsx',
195
+ },
196
+ layout: './layouts/Root.tsx',
197
+ notFound: './pages/NotFound.tsx',
198
+ tailwind: { entry: './styles/input.css' },
199
+ }))
185
200
 
186
- app.use(react({
187
- layout: ({ children }) =>
188
- h('html', null,
189
- h('head', null, h('title', null, 'My App')),
190
- h('body', null, h('div', { id: 'root' }, children)),
191
- ),
192
- }))
201
+ app.get('/api/hello', () => Response.json({ message: 'hi' }))
202
+ serve(app, { port: 3000 })
203
+ ```
204
+
205
+ **One `react()` call handles:** SSR rendering, routing, data loading, Tailwind CSS, client bundle auto-generation, and error pages. No `client.ts`, `routes.ts`, or manual middleware setup needed.
206
+
207
+ #### Page components
208
+
209
+ ```tsx
210
+ // pages/UserDetail.tsx
211
+ import type { Context } from 'weifuwu'
212
+ import { HttpError } from 'weifuwu'
213
+ import { useServerData } from 'weifuwu/react'
193
214
 
194
- // Render React components to HTML
195
- app.get('/', (_req, ctx) =>
196
- ctx.render(h('h1', null, 'Hello SSR'), {
197
- head: { title: 'Home' },
198
- data: { greeting: 'Hello' },
199
- })
200
- )
201
-
202
- // Streaming SSR
203
- app.get('/dashboard', (_req, ctx) =>
204
- ctx.renderStream(h(Dashboard))
205
- )
206
-
207
- // Layout nesting via mount
208
- const admin = new Router()
209
- admin.use(react({ layout: AdminLayout }))
210
- admin.get('/dashboard', (_req, ctx) => ctx.render(h(AdminDashboard)))
211
- app.mount('/admin', admin)
212
-
213
- // ── Shared components ──────────────────────────────────
214
-
215
- // Use the same component on server and client
216
- function Page() {
217
- const { greeting } = useServerData<{ greeting: string }>()
218
- return h('h1', null, greeting)
215
+ export async function loader(ctx: Context) {
216
+ const user = await db.find(ctx.params.id)
217
+ if (!user) throw new HttpError('Not found', 404)
218
+ return { user }
219
219
  }
220
220
 
221
- // Link works on both sides: <a> on server, SPA <a> on client
222
- function Nav() {
223
- return h('nav', null,
224
- h(Link, { href: '/' }, 'Home'),
225
- h(Link, { href: '/users' }, 'Users'),
221
+ export default function UserDetailPage() {
222
+ const { user } = useServerData<{ user: User }>()
223
+ return (
224
+ <div>
225
+ <title>{`${user.name} My App`}</title>
226
+ <h1>{user.name}</h1>
227
+ <p>{user.email}</p>
228
+ </div>
226
229
  )
227
230
  }
231
+ ```
232
+
233
+ - `export async function loader(ctx)` — runs on the server, returns data for `useServerData()`
234
+ - `throw new HttpError('Not found', 404)` — renders the NotFound page with correct status
235
+ - `<title>` auto-hoists to `<head>` via React 19
236
+ - `export default` is auto-detected; named exports work too
228
237
 
229
- // ErrorBoundary catches client-side render errors
230
- function SafeUserProfile() {
231
- return h(ErrorBoundary, { fallback: h('div', null, 'Error') },
232
- h(UserProfile),
238
+ #### Layout with shared data
239
+
240
+ ```tsx
241
+ // layouts/Root.tsx
242
+ export async function loader(ctx: Context) {
243
+ return { currentUser: await getCurrentUser(ctx) }
244
+ }
245
+
246
+ export function Root({ children }: { children: ReactNode }) {
247
+ const { currentUser } = useServerData()
248
+ return (
249
+ <>
250
+ <nav>
251
+ <a href="/">Home</a>
252
+ <a href="/users">Users</a>
253
+ <span>{currentUser?.name}</span>
254
+ </nav>
255
+ <main>{children}</main>
256
+ </>
233
257
  )
234
258
  }
259
+ ```
235
260
 
236
- // ── Client (SPA navigation) ────────────────────────────
261
+ Layout `loader` data merges with page data. Page loader overrides same keys.
237
262
 
238
- // client.ts — bundled separately with esbuild
239
- import { hydrate, createClientRouter, defineRoute } from 'weifuwu/react/client'
263
+ #### Data flow
240
264
 
241
- const userRoute = defineRoute({
242
- path: '/users/:id',
243
- component: UserPage,
244
- loader: (params) => fetch(`/users/${params.id}?_data`).then(r => r.json()),
245
- })
265
+ ```
266
+ layout loader ──┐
267
+ ├──→ merge → useServerData() ──→ Layout + Page
268
+ page loader ───┘
269
+ ```
270
+
271
+ #### Client-side SPA
272
+
273
+ Every page is automatically code-split into a separate chunk. On first visit the server renders HTML and the client hydrates. Subsequent navigations intercept `<a>` clicks, `import()` the page chunk, and fetch fresh server data — all automatic, zero config.
246
274
 
247
- const router = createClientRouter([
248
- { path: '/', component: HomePage },
249
- userRoute,
250
- ])
275
+ #### Streaming SSR
251
276
 
252
- hydrate(router.App)
277
+ ```tsx
278
+ import { Suspense, use } from 'react'
279
+
280
+ export default function StreamingPage() {
281
+ return (
282
+ <div>
283
+ <h1>Instant shell</h1>
284
+ <Suspense fallback={<Spinner />}>
285
+ <SlowData promise={fetchSlowData()} />
286
+ </Suspense>
287
+ </div>
288
+ )
289
+ }
253
290
  ```
254
291
 
255
- **Key concepts:**
292
+ `<Suspense>` boundaries stream to the browser as data resolves.
256
293
 
257
294
  | Feature | Description |
258
295
  |---|---|
259
- | `ctx.render(el, opts)` | Render React to HTML. Auto-detects `?_data` returns JSON. |
260
- | `ctx.renderStream(el)` | Streaming SSR via `renderToReadableStream`. |
261
- | `useServerData<T>()` | Access data on both server (via `ctx.render({ data })`) and client (via `loader`). |
262
- | `Link` | Shared component — `<a>` on server, SPA `<a>` on client. |
263
- | `Form` | Shared `<form>` on server, `fetch` + revalidate on client. |
264
- | `ErrorBoundary` | Catches render errors on client (SSR uses `onError`). |
265
- | `useNavigation()` | `{ state: 'idle' \| 'loading' }` for progress indicators. |
266
- | `useParams()` / `useNavigate()` / `useRevalidate()` | Router hooks. |
267
- | `defineRoute()` | Type-safe route config — captures loader return type. |
268
- | `createClientRouter()` | Client-side SPA router with loader-based data fetching. |
269
- | `hydrate(App)` | Hydrate server-rendered HTML on the client. |
270
- | `head: { title, meta }` | Inject dynamic `<title>` and `<meta>` tags. |
296
+ | `react({ pages, layout, notFound, tailwind })` | One call: SSR + routing + client bundle + error handling |
297
+ | `export async function loader(ctx)` | Server-side data loading, auto-detected by the framework |
298
+ | `useServerData<T>()` | Type-safe access to loader data in any component |
299
+ | `Link` | `<a>` that does SPA navigation on the client |
300
+ | `ErrorBoundary` | Catches render errors on server and client |
301
+ | `<title>`, `<meta>` | Auto-hoisted to `<head>` via React 19 |
302
+ | `<Suspense>` | Streaming SSR, works out of the box |
303
+ | `client: false` | Disable client JS for static SSR only |
271
304
 
272
305
  **Import paths:**
273
306
 
274
307
  ```ts
275
308
  // Server-side
276
- import { react, Link, Form, ErrorBoundary, useServerData } from 'weifuwu'
309
+ import { react, Link, ErrorBoundary, useServerData } from 'weifuwu'
310
+
311
+ // Client-side (for custom advanced setups)
312
+ import { createBrowserRouter, hydrate, navigate } from 'weifuwu/react/client'
313
+ ```
314
+
315
+ See [examples/react-ssr/](examples/react-ssr/) for the full demo.
277
316
 
278
- // Client-side (for browser bundles)
279
- import { hydrate, createClientRouter, defineRoute, Link } from 'weifuwu/react/client'
317
+ ### AI Agent
280
318
 
281
- // Shared primitives (safe for both, pure React no react-dom import)
282
- import { Link, Form, useServerData, useParams, useNavigation } from 'weifuwu/react/navigation'
319
+ > Requires `ai` (Vercel AI SDK). Install with a model provider:
320
+ > ```bash
321
+ > npm install ai @ai-sdk/openai
322
+ > ```
323
+
324
+ ```ts
325
+ import { agent } from 'weifuwu'
326
+ import { openai } from '@ai-sdk/openai'
327
+ import { tool } from 'ai'
328
+ import { z } from 'zod'
329
+
330
+ app.use(agent({
331
+ model: openai('gpt-4o'),
332
+ system: 'You are a helpful assistant.',
333
+ knowledge: {
334
+ // RAG — search a knowledge base before each response
335
+ search: async (query, ctx) => {
336
+ const { embeddings } = await embedModel.doEmbed({ values: [query] })
337
+ return ctx.sql`
338
+ SELECT content, 1 - (embedding <=> ${embeddings[0]}::vector) AS score
339
+ FROM docs ORDER BY embedding <=> ${embeddings[0]}::vector LIMIT 3
340
+ `
341
+ },
342
+ },
343
+ tools: {
344
+ getWeather: tool({
345
+ description: 'Get weather for a city',
346
+ parameters: z.object({ city: z.string() }),
347
+ execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
348
+ }),
349
+ },
350
+ maxSteps: 5,
351
+ }))
352
+
353
+ app.post('/api/chat', async (req, ctx) => {
354
+ const { messages } = await req.json()
355
+ return ctx.agent.chatStreamResponse({ messages })
356
+ })
283
357
  ```
284
358
 
285
- See [examples/react-ssr/](examples/react-ssr/) for a complete demo.
359
+ **`agent()` handles:** multi-turn conversations, automatic tool-calling loops (maxSteps), knowledge retrieval (RAG) injected into the system prompt, and SSE streaming compatible with `useChat` from `@ai-sdk/react`.
360
+
361
+ | Feature | Description |
362
+ |---|---|
363
+ | `ctx.agent.chat(prompt, opts?)` | Non-streaming chat with tool calling and RAG |
364
+ | `ctx.agent.chatStreamResponse({ messages })` | SSE streaming response (useChat-compatible) |
365
+ | `knowledge.search` | User-defined RAG callback — query any data source via `ctx` |
366
+ | `tools` | Tool definitions (from `ai` package). Executed in automatic loops |
367
+ | `agents` | Named sub-agents with different models/tools |
368
+ | `sandbox: true` | Auto-integrate with `ctx.sandbox` for file operations |
369
+ | `store` | Session persistence (save/load conversation history) |
370
+
371
+ ### Auth
372
+
373
+ ```ts
374
+ import { auth } from 'weifuwu'
375
+
376
+ // JWT
377
+ app.use(auth({ jwt: { secret: process.env.JWT_SECRET } }))
378
+
379
+ // Session cookie
380
+ app.use(auth({
381
+ session: {
382
+ secret: '...',
383
+ loadUser: async (data) => db.findUser(data.userId),
384
+ },
385
+ }))
386
+
387
+ // API key
388
+ app.use(auth({ apiKey: { validate: async (key) => db.findByApiKey(key) } }))
389
+
390
+ // ctx.user is now available
391
+ app.get('/me', (req, ctx) => {
392
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
393
+ return Response.json(ctx.user)
394
+ })
395
+ ```
396
+
397
+ Supports JWT (HS256 via cookie or Authorization header), signed session cookies with `loadUser` callback, and API key validation. All are optional — requests proceed without a user identity unless handlers enforce it.
398
+
399
+ ### Sandbox
400
+
401
+ ```ts
402
+ import { sandbox } from 'weifuwu'
403
+
404
+ app.use(sandbox({
405
+ baseDir: '/tmp/workspaces',
406
+ timeout: 30000,
407
+ isolateBy: 'user', // one directory per ctx.user.id
408
+ }))
409
+
410
+ // ctx.sandbox provides isolated file + exec operations
411
+ await ctx.sandbox.writeFile('hello.txt', 'world')
412
+ const content = await ctx.sandbox.readFile('hello.txt')
413
+ const { stdout } = await ctx.sandbox.exec('ls -la')
414
+ await ctx.sandbox.destroy() // clean up workspace
415
+ ```
416
+
417
+ All file paths are validated — escapes (`../`) are rejected. `exec()` enforces timeout and sets `HOME` to the workspace directory. When `isolateBy: 'user'` is set, each user gets their own directory under `baseDir`.
286
418
 
287
419
  ### Types
288
420
 
@@ -339,50 +471,35 @@ app.onError((err, req, ctx) => {
339
471
  ## Complete example
340
472
 
341
473
  ```ts
342
- import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, queue, HttpError } from 'weifuwu'
474
+ import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, auth, sandbox, HttpError } from 'weifuwu'
475
+ import { agent } from 'weifuwu/agent'
476
+ import { react } from 'weifuwu/react'
477
+ import { openai } from '@ai-sdk/openai'
343
478
 
344
479
  const app = new Router()
345
-
346
- // Global middleware
347
- app.use(trace())
348
- app.use(logger())
349
- app.use(cors())
350
- app.use(helmet())
351
- app.use(compress())
352
- app.use(rateLimit({ windowMs: 60_000, max: 100 }))
353
-
354
- // Database
355
- const sql = postgres()
356
- app.use(sql)
357
-
358
- // Routes
359
- app.get('/api/users', async (req, ctx) => {
360
- const users = await ctx.sql`SELECT id, name FROM users ORDER BY id`
361
- return Response.json(users)
362
- })
363
-
364
- app.post('/api/users', async (req, ctx) => {
365
- const { name, email } = await req.json()
366
- const [user] = await ctx.sql`INSERT INTO users (name,email) VALUES (${name},${email}) RETURNING *`
367
- return Response.json(user, { status: 201 })
368
- })
369
-
370
- app.get('/api/users/:id', async (req, ctx) => {
371
- const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
372
- if (!user) throw new HttpError('Not found', 404)
373
- return Response.json(user)
374
- })
375
-
376
- // WebSocket
377
- app.ws('/chat', {
378
- open(ws, ctx) { ctx.ws.join('lobby') },
379
- message(ws, ctx, data) { ctx.ws.sendRoom('lobby', { text: data.toString() }) },
380
- })
381
-
382
- // Error handling
383
- app.onError((err) => {
384
- if (err instanceof HttpError) return Response.json({ error: err.message }, { status: err.status })
385
- return Response.json({ error: 'Internal error' }, { status: 500 })
480
+ .use(trace())
481
+ .use(logger())
482
+ .use(cors())
483
+ .use(helmet())
484
+ .use(compress())
485
+ .use(postgres())
486
+ .use(redis())
487
+ .use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
488
+ .use(sandbox({ isolateBy: 'user' }))
489
+ .use(agent({
490
+ model: openai('gpt-4o'),
491
+ system: 'You are a helpful assistant.',
492
+ knowledge: {
493
+ search: async (q, ctx) => ctx.sql`...`,
494
+ },
495
+ sandbox: true,
496
+ }))
497
+ .plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
498
+
499
+ app.post('/api/chat', async (req, ctx) => {
500
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
501
+ const { messages } = await req.json()
502
+ return ctx.agent.chatStreamResponse({ messages })
386
503
  })
387
504
 
388
505
  serve(app, { port: 3000 })
@@ -403,6 +520,7 @@ weifuwu/
403
520
  │ ├── types.ts
404
521
  │ ├── core/ ← serve, router, ws, trace, logger
405
522
  │ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
523
+ │ ├── ai/ ← AI + agent middleware
406
524
  │ ├── postgres/
407
525
  │ ├── redis/
408
526
  │ ├── react/ ← react SSR (render, navigation, client)
@@ -0,0 +1,127 @@
1
+ import type { Middleware, Context } from '../types.ts';
2
+ import type { AiOptions } from '../ai/types.ts';
3
+ declare module '../types.ts' {
4
+ interface Context {
5
+ /**
6
+ * AI agent — LLM integration with session management, tool calling,
7
+ * knowledge retrieval, and streaming.
8
+ * Injected by the `agent()` middleware.
9
+ */
10
+ agent: Agent;
11
+ }
12
+ }
13
+ export interface Agent {
14
+ /**
15
+ * Chat with the agent (non-streaming). Returns the final response
16
+ * after tool calls and knowledge retrieval.
17
+ */
18
+ chat(prompt: string, opts?: AgentChatOptions): Promise<AgentChatResult>;
19
+ /**
20
+ * Stream the agent's response as an SSE Response.
21
+ * Compatible with `useChat` from @ai-sdk/react.
22
+ */
23
+ chatStreamResponse(opts: AgentStreamOptions): Response;
24
+ /** The underlying language model. */
25
+ model: unknown;
26
+ /** Default system prompt. */
27
+ system: string | undefined;
28
+ }
29
+ export interface AgentChatOptions {
30
+ /** Override system prompt for this call. */
31
+ system?: string;
32
+ /** Chat history messages (from useChat). */
33
+ messages?: Array<{
34
+ role: string;
35
+ content: string;
36
+ [key: string]: unknown;
37
+ }>;
38
+ /** Override temperature. */
39
+ temperature?: number;
40
+ /** Override max tokens. */
41
+ maxTokens?: number;
42
+ /** Override max steps. */
43
+ maxSteps?: number;
44
+ }
45
+ export interface AgentStreamOptions {
46
+ /** User prompt or full message list. */
47
+ messages: Array<{
48
+ role: string;
49
+ content: string;
50
+ [key: string]: unknown;
51
+ }>;
52
+ /** Override system prompt. */
53
+ system?: string;
54
+ }
55
+ export type AgentChatResult = Record<string, any>;
56
+ export interface AgentOptions extends AiOptions {
57
+ /**
58
+ * Knowledge base for RAG. Called before every chat to inject context.
59
+ */
60
+ knowledge?: {
61
+ /**
62
+ * Search for relevant documents given a user query.
63
+ * Results are injected into the system prompt.
64
+ */
65
+ search: (query: string, ctx: Context) => Promise<Array<{
66
+ content: string;
67
+ score?: number;
68
+ }>>;
69
+ /** Maximum results to inject (default: 3). */
70
+ topK?: number;
71
+ /** Minimum relevance score (default: 0). */
72
+ minScore?: number;
73
+ };
74
+ /**
75
+ * Named sub-agents with their own model, system prompt, and tools.
76
+ * Access via ctx.agent.agents[name].
77
+ */
78
+ agents?: Record<string, {
79
+ model?: unknown;
80
+ system?: string;
81
+ tools?: Record<string, unknown>;
82
+ }>;
83
+ /**
84
+ * Enable sandbox integration. When true and ctx.sandbox is available,
85
+ * the agent automatically uses it for file operations.
86
+ */
87
+ sandbox?: boolean;
88
+ /**
89
+ * Store for session persistence. Save/load conversation history.
90
+ */
91
+ store?: {
92
+ save: (sessionId: string, messages: unknown[]) => Promise<void>;
93
+ load: (sessionId: string) => Promise<unknown[]>;
94
+ };
95
+ }
96
+ /**
97
+ * AI Agent middleware — injects `ctx.agent` for LLM-powered conversations.
98
+ *
99
+ * Built on the Vercel AI SDK. Supports:
100
+ * - Multi-turn conversations with session history
101
+ * - Tool calling with automatic loops (maxSteps)
102
+ * - Knowledge retrieval (RAG) via `knowledge.search`
103
+ * - Streaming SSE responses compatible with `useChat` from @ai-sdk/react
104
+ * - Named sub-agents for role-based delegation
105
+ * - Sandbox integration
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { agent } from 'weifuwu'
110
+ * import { openai } from '@ai-sdk/openai'
111
+ *
112
+ * app.use(agent({
113
+ * model: openai('gpt-4o'),
114
+ * system: 'You are a helpful assistant.',
115
+ * knowledge: {
116
+ * search: async (query, ctx) => ctx.sql`...pgvector...`,
117
+ * },
118
+ * tools: { getWeather: tool({...}) },
119
+ * }))
120
+ *
121
+ * app.post('/api/chat', async (req, ctx) => {
122
+ * const { messages } = await req.json()
123
+ * return ctx.agent.chatStreamResponse({ messages })
124
+ * })
125
+ * ```
126
+ */
127
+ export declare function agent(opts: AgentOptions): Middleware;
@@ -0,0 +1,26 @@
1
+ import type { Middleware } from '../types.ts';
2
+ import type { AiOptions } from './types.ts';
3
+ export type { AiOptions, Ai } from './types.ts';
4
+ /**
5
+ * AI middleware — injects `ctx.ai` for LLM integration via the Vercel AI SDK.
6
+ *
7
+ * Requires a language model provider (e.g. `@ai-sdk/openai`, `@ai-sdk/anthropic`).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { ai } from 'weifuwu'
12
+ * import { openai } from '@ai-sdk/openai'
13
+ *
14
+ * app.use(ai({
15
+ * model: openai('gpt-4o'),
16
+ * system: 'You are a helpful assistant.',
17
+ * }))
18
+ *
19
+ * app.post('/api/chat', async (req, ctx) => {
20
+ * const { prompt } = await req.json()
21
+ * const result = await ctx.ai.generateText({ prompt })
22
+ * return Response.json({ text: result.text })
23
+ * })
24
+ * ```
25
+ */
26
+ export declare function ai(opts: AiOptions): Middleware;
@@ -0,0 +1,59 @@
1
+ declare module '../types.ts' {
2
+ interface Context {
3
+ /** AI / LLM integration. Injected by the `ai()` middleware. */
4
+ ai: Ai;
5
+ }
6
+ }
7
+ export interface Ai {
8
+ /**
9
+ * Generate text (non-streaming). See `generateText` from the `ai` package.
10
+ *
11
+ * @example
12
+ * const { text, toolCalls, steps } = await ctx.ai.generateText({
13
+ * prompt: 'What is the weather?',
14
+ * })
15
+ */
16
+ generateText(opts: GenerateTextParams): Promise<AiGenerateTextResult>;
17
+ /** The language model instance. */
18
+ model: unknown;
19
+ /** Default system prompt. */
20
+ system: string | undefined;
21
+ }
22
+ export type AiGenerateTextResult = Awaited<ReturnType<typeof import('ai').generateText<any, any, any>>>;
23
+ export interface AiOptions {
24
+ /**
25
+ * Language model instance (from @ai-sdk/openai, @ai-sdk/anthropic, etc.).
26
+ *
27
+ * @example
28
+ * import { openai } from '@ai-sdk/openai'
29
+ * ai({ model: openai('gpt-4o') })
30
+ */
31
+ model: unknown;
32
+ /** System prompt added to every request. */
33
+ system?: string;
34
+ /** Tools available for tool calling. */
35
+ tools?: any;
36
+ /** Maximum agent steps for tool-calling loops (default: 1). */
37
+ maxSteps?: number;
38
+ /** Default temperature (0–2). */
39
+ temperature?: number;
40
+ /** Default max output tokens. */
41
+ maxTokens?: number;
42
+ }
43
+ export interface GenerateTextParams {
44
+ /** User prompt. */
45
+ prompt: string;
46
+ /** System prompt override. */
47
+ system?: string;
48
+ /** Chat history messages. */
49
+ messages?: Array<{
50
+ role: string;
51
+ content: string;
52
+ } & Record<string, any>>;
53
+ /** Override temperature. */
54
+ temperature?: number;
55
+ /** Override max tokens. */
56
+ maxTokens?: number;
57
+ /** Override max steps. */
58
+ maxSteps?: number;
59
+ }
@@ -25,6 +25,17 @@ export declare class Router<T extends Context = Context> {
25
25
  private get hub();
26
26
  wsHub(hub: Hub): this;
27
27
  use(mw: Middleware<Context, Context>): Router<T>;
28
+ /**
29
+ * Install a plugin — a function that configures the Router with
30
+ * routes, middleware, and error handlers. Use when `.use()` isn't
31
+ * enough because you need to call `app.get()`, `app.onError()`, etc.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * app.plugin(app => createReactApp(app, { pages: {...}, layout: '...', tailwind: {...} }))
36
+ * ```
37
+ */
38
+ plugin(fn: (app: this) => void): this;
28
39
  mount(path: string, router: Router<Context>): Router<T>;
29
40
  onError(handler: ErrorHandler<T>): Router<T>;
30
41
  get(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;