@ai-me-chat/nextjs 0.0.1 → 0.2.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 +152 -0
- package/dist/index.cjs +56 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +56 -24
- package/dist/index.js.map +1 -1
- package/package.json +18 -4
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @ai-me-chat/nextjs
|
|
2
|
+
|
|
3
|
+
Next.js integration for AI-Me — route handler factory with auto-discovery, filtering, and auth forwarding.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ai-me-chat/nextjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
**Important:** Use an optional catch-all route so sub-paths (`/tools`, `/health`) are handled:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
app/api/ai-me/[[...path]]/route.ts <-- correct
|
|
17
|
+
app/api/ai-me/route.ts <-- won't handle /tools or /health
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Create `app/api/ai-me/[[...path]]/route.ts`:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createAIMeHandler } from "@ai-me-chat/nextjs";
|
|
24
|
+
import { openai } from "@ai-sdk/openai";
|
|
25
|
+
|
|
26
|
+
const handler = createAIMeHandler({
|
|
27
|
+
model: openai("gpt-4o"),
|
|
28
|
+
|
|
29
|
+
discovery: {
|
|
30
|
+
mode: "filesystem",
|
|
31
|
+
include: ["/api/**"],
|
|
32
|
+
exclude: ["/api/ai-me/**"],
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
getSession: async (req) => {
|
|
36
|
+
// your auth logic
|
|
37
|
+
return { user: { id: "user-1", role: "admin" } };
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
systemPrompt: "You are a helpful AI assistant for this app.",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export { handler as GET, handler as POST };
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Filesystem discovery** — auto-scans `app/api/` routes at startup, with `src/app` auto-detection
|
|
49
|
+
- **OpenAPI discovery** — generate tools from an OpenAPI 3.x spec (inline or remote URL)
|
|
50
|
+
- **Route filtering** — include/exclude patterns with glob support
|
|
51
|
+
- **Auth forwarding** — forwards cookies and authorization headers to your routes
|
|
52
|
+
- **Write confirmation** — POST/PUT/PATCH/DELETE require user confirmation by default
|
|
53
|
+
- **Dynamic system prompt** — inject user-specific context via a function
|
|
54
|
+
|
|
55
|
+
## Endpoints
|
|
56
|
+
|
|
57
|
+
| Path | Method | Purpose | Auth |
|
|
58
|
+
|------|--------|---------|------|
|
|
59
|
+
| `/api/ai-me` | POST | Chat endpoint | Required |
|
|
60
|
+
| `/api/ai-me/tools` | GET | List discovered tools | Required |
|
|
61
|
+
| `/api/ai-me/health` | GET | Health check | **No** |
|
|
62
|
+
|
|
63
|
+
The health endpoint does NOT require authentication — use it for liveness probes and monitoring.
|
|
64
|
+
|
|
65
|
+
## App Directory Detection
|
|
66
|
+
|
|
67
|
+
When using `mode: "filesystem"`, the handler automatically locates your Next.js app directory:
|
|
68
|
+
|
|
69
|
+
1. `src/app` — checked first (default for `npx create-next-app`)
|
|
70
|
+
2. `app` — fallback for projects without a `src/` layout
|
|
71
|
+
|
|
72
|
+
You can override this by setting `appDir` in the discovery config:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
discovery: {
|
|
76
|
+
mode: "filesystem",
|
|
77
|
+
appDir: "src/app", // relative to project root, or absolute
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## OpenAPI Discovery Mode
|
|
82
|
+
|
|
83
|
+
Instead of scanning the filesystem, provide an OpenAPI 3.x spec (inline or remote):
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
createAIMeHandler({
|
|
87
|
+
discovery: {
|
|
88
|
+
mode: "openapi",
|
|
89
|
+
spec: {
|
|
90
|
+
openapi: "3.0.3",
|
|
91
|
+
info: { title: "My API", version: "1.0.0" },
|
|
92
|
+
paths: {
|
|
93
|
+
"/api/users": {
|
|
94
|
+
get: {
|
|
95
|
+
operationId: "listUsers",
|
|
96
|
+
summary: "List all users",
|
|
97
|
+
parameters: [
|
|
98
|
+
{ name: "q", in: "query", schema: { type: "string" }, description: "Search by name" }
|
|
99
|
+
],
|
|
100
|
+
responses: { "200": { description: "User list" } }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
...
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or fetch from a remote URL:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
discovery: {
|
|
114
|
+
mode: "openapi",
|
|
115
|
+
specUrl: "http://localhost:3000/api/openapi.json"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**When to use OpenAPI mode:**
|
|
120
|
+
- Your app uses `src/app` and filesystem detection fails
|
|
121
|
+
- You want custom tool names via `operationId`
|
|
122
|
+
- You want rich parameter descriptions for better AI understanding
|
|
123
|
+
- You want to expose only specific endpoints (include/exclude still work)
|
|
124
|
+
|
|
125
|
+
## Dynamic System Prompt
|
|
126
|
+
|
|
127
|
+
Inject user-specific context by passing a function:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
createAIMeHandler({
|
|
131
|
+
systemPrompt: async (session) => {
|
|
132
|
+
const settings = await db.settings.findUnique({ where: { userId: session.user.id } });
|
|
133
|
+
return `You are an assistant for ${settings.companyName}. The user is ${session.user.name}.`;
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Peer Dependencies
|
|
139
|
+
|
|
140
|
+
| Package | Version |
|
|
141
|
+
|---------|---------|
|
|
142
|
+
| `ai` | ^6.0.0 |
|
|
143
|
+
| `next` | ^16.0.0 |
|
|
144
|
+
| `react` | ^19.0.0 |
|
|
145
|
+
|
|
146
|
+
## Documentation
|
|
147
|
+
|
|
148
|
+
Full setup guide and API reference: [github.com/aselims/ai-me-chat](https://github.com/aselims/ai-me-chat)
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -125,20 +125,34 @@ function filterRoutes(routes, config) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// src/handler.ts
|
|
128
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
129
|
+
var import_path = __toESM(require("path"), 1);
|
|
128
130
|
var import_ai = require("ai");
|
|
129
131
|
var import_core = require("@ai-me-chat/core");
|
|
132
|
+
function detectAppDir() {
|
|
133
|
+
const srcApp = import_path.default.join(process.cwd(), "src", "app");
|
|
134
|
+
if (import_fs.default.existsSync(srcApp)) return srcApp;
|
|
135
|
+
return import_path.default.join(process.cwd(), "app");
|
|
136
|
+
}
|
|
137
|
+
function resolveAppDir(config) {
|
|
138
|
+
if (config.discovery.appDir) {
|
|
139
|
+
return import_path.default.resolve(process.cwd(), config.discovery.appDir);
|
|
140
|
+
}
|
|
141
|
+
return detectAppDir();
|
|
142
|
+
}
|
|
130
143
|
function createAIMeHandler(config) {
|
|
144
|
+
const appDir = resolveAppDir(config);
|
|
131
145
|
let toolDefinitions = null;
|
|
132
146
|
let toolsPromise = null;
|
|
133
|
-
async function getToolDefinitions(
|
|
147
|
+
async function getToolDefinitions() {
|
|
134
148
|
if (toolDefinitions) return toolDefinitions;
|
|
135
149
|
if (toolsPromise) return toolsPromise;
|
|
136
|
-
toolsPromise = initTools(
|
|
150
|
+
toolsPromise = initTools();
|
|
137
151
|
toolDefinitions = await toolsPromise;
|
|
138
152
|
toolsPromise = null;
|
|
139
153
|
return toolDefinitions;
|
|
140
154
|
}
|
|
141
|
-
async function initTools(
|
|
155
|
+
async function initTools() {
|
|
142
156
|
if (config.discovery.mode === "openapi") {
|
|
143
157
|
let spec;
|
|
144
158
|
if (config.discovery.spec) {
|
|
@@ -171,20 +185,22 @@ function createAIMeHandler(config) {
|
|
|
171
185
|
return (0, import_core.generateToolDefinitions)(routes, config.confirmation);
|
|
172
186
|
}
|
|
173
187
|
async function handler(req) {
|
|
188
|
+
const url = new URL(req.url);
|
|
189
|
+
if (req.method === "GET" && url.pathname.endsWith("/health")) {
|
|
190
|
+
return Response.json({ status: "ok" });
|
|
191
|
+
}
|
|
174
192
|
const session = await config.getSession(req);
|
|
175
193
|
if (!session) {
|
|
176
194
|
return new Response("Unauthorized", { status: 401 });
|
|
177
195
|
}
|
|
178
|
-
const url = new URL(req.url);
|
|
179
196
|
if (req.method === "GET" && url.pathname.endsWith("/tools")) {
|
|
180
|
-
const appDir = process.cwd() + "/app";
|
|
181
197
|
let tools;
|
|
182
198
|
try {
|
|
183
|
-
tools = await getToolDefinitions(
|
|
199
|
+
tools = await getToolDefinitions();
|
|
184
200
|
} catch (error) {
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
{ status: 500
|
|
201
|
+
return Response.json(
|
|
202
|
+
{ error: error instanceof Error ? error.message : "Tool discovery failed" },
|
|
203
|
+
{ status: 500 }
|
|
188
204
|
);
|
|
189
205
|
}
|
|
190
206
|
return Response.json(
|
|
@@ -197,21 +213,36 @@ function createAIMeHandler(config) {
|
|
|
197
213
|
}))
|
|
198
214
|
);
|
|
199
215
|
}
|
|
200
|
-
if (req.method === "GET" && url.pathname.endsWith("/health")) {
|
|
201
|
-
return Response.json({ status: "ok", version: "0.0.1" });
|
|
202
|
-
}
|
|
203
216
|
if (req.method === "POST") {
|
|
204
|
-
return handleChat(req, config, session, getToolDefinitions);
|
|
217
|
+
return handleChat(req, config, session, getToolDefinitions, url.origin);
|
|
205
218
|
}
|
|
206
219
|
return new Response("Not Found", { status: 404 });
|
|
207
220
|
}
|
|
208
221
|
return handler;
|
|
209
222
|
}
|
|
210
|
-
async function handleChat(req, config, session, getToolDefinitions) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
async function handleChat(req, config, session, getToolDefinitions, baseUrl) {
|
|
224
|
+
let body;
|
|
225
|
+
try {
|
|
226
|
+
body = await req.json();
|
|
227
|
+
} catch {
|
|
228
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
229
|
+
}
|
|
230
|
+
const { messages } = body;
|
|
231
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
232
|
+
return Response.json({ error: "messages must be a non-empty array" }, { status: 400 });
|
|
233
|
+
}
|
|
234
|
+
for (const msg of messages) {
|
|
235
|
+
if (!msg.id || !msg.role) {
|
|
236
|
+
return Response.json(
|
|
237
|
+
{
|
|
238
|
+
error: "Each message must have an 'id' and 'role' field. Use the AI SDK UIMessage format.",
|
|
239
|
+
received: Object.keys(msg)
|
|
240
|
+
},
|
|
241
|
+
{ status: 400 }
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const toolDefs = await getToolDefinitions();
|
|
215
246
|
const executionContext = {
|
|
216
247
|
baseUrl,
|
|
217
248
|
headers: {
|
|
@@ -221,17 +252,16 @@ async function handleChat(req, config, session, getToolDefinitions) {
|
|
|
221
252
|
};
|
|
222
253
|
const aiTools = {};
|
|
223
254
|
for (const toolDef of toolDefs) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
description: def.description,
|
|
255
|
+
aiTools[toolDef.name] = (0, import_ai.tool)({
|
|
256
|
+
description: toolDef.description,
|
|
227
257
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
-
inputSchema:
|
|
258
|
+
inputSchema: toolDef.parameters,
|
|
229
259
|
// Disable strict schema validation for OpenAI-compatible providers
|
|
230
260
|
// (e.g., Groq) that reject additionalProperties in tool schemas
|
|
231
261
|
strict: false,
|
|
232
262
|
execute: async (params) => {
|
|
233
263
|
const result2 = await (0, import_core.executeTool)(
|
|
234
|
-
|
|
264
|
+
toolDef,
|
|
235
265
|
params,
|
|
236
266
|
executionContext
|
|
237
267
|
);
|
|
@@ -242,9 +272,11 @@ async function handleChat(req, config, session, getToolDefinitions) {
|
|
|
242
272
|
const modelMessages = await (0, import_ai.convertToModelMessages)(messages);
|
|
243
273
|
const maxHistory = config.maxHistoryMessages ?? 20;
|
|
244
274
|
const trimmedMessages = modelMessages.length > maxHistory ? modelMessages.slice(-maxHistory) : modelMessages;
|
|
275
|
+
const defaultPrompt = `You are an AI assistant for this application. You can help users query data and perform actions. User: ${session.user.id}${session.user.role ? ` (role: ${session.user.role})` : ""}`;
|
|
276
|
+
const systemPromptValue = typeof config.systemPrompt === "function" ? await config.systemPrompt(session) : config.systemPrompt ?? defaultPrompt;
|
|
245
277
|
const result = (0, import_ai.streamText)({
|
|
246
278
|
model: config.model,
|
|
247
|
-
system:
|
|
279
|
+
system: systemPromptValue,
|
|
248
280
|
messages: trimmedMessages,
|
|
249
281
|
tools: aiTools,
|
|
250
282
|
stopWhen: (0, import_ai.stepCountIs)(5),
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/scanner.ts","../src/filter.ts","../src/handler.ts"],"sourcesContent":["export { scanRoutes } from \"./scanner.js\";\nexport { filterRoutes } from \"./filter.js\";\nexport { createAIMeHandler } from \"./handler.js\";\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { DiscoveredRoute } from \"@ai-me-chat/core\";\n\nconst HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\n\nconst ROUTE_FILE_NAMES = [\"route.ts\", \"route.js\", \"route.tsx\", \"route.jsx\"];\n\n/**\n * Scan a Next.js App Router directory for API routes.\n * Finds all route.ts/route.js files under `appDir/api/` and extracts\n * HTTP methods and path parameters.\n */\nexport function scanRoutes(appDir: string): DiscoveredRoute[] {\n const apiDir = path.join(appDir, \"api\");\n if (!fs.existsSync(apiDir)) {\n return [];\n }\n const routes: DiscoveredRoute[] = [];\n walkDirectory(apiDir, appDir, routes);\n return routes.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nfunction walkDirectory(\n dir: string,\n appDir: string,\n routes: DiscoveredRoute[],\n): void {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walkDirectory(fullPath, appDir, routes);\n } else if (ROUTE_FILE_NAMES.includes(entry.name)) {\n const route = parseRouteFile(fullPath, appDir);\n if (route && route.methods.length > 0) {\n routes.push(route);\n }\n }\n }\n}\n\nfunction parseRouteFile(filePath: string, appDir: string): DiscoveredRoute | null {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const methods = extractHttpMethods(content);\n\n if (methods.length === 0) {\n return null;\n }\n\n const relativePath = path.relative(appDir, path.dirname(filePath));\n const apiPath = \"/\" + relativePath.split(path.sep).join(\"/\");\n const pathParams = extractPathParams(apiPath);\n\n return {\n path: cleanPath(apiPath),\n methods,\n pathParams,\n filePath: path.relative(path.resolve(appDir, \"..\"), filePath),\n };\n}\n\n/**\n * Extract exported HTTP method handlers from route file content.\n * Matches patterns like:\n * export async function GET(...)\n * export function POST(...)\n * export const PUT = ...\n * export { handler as DELETE }\n */\nfunction extractHttpMethods(content: string): string[] {\n const methods: string[] = [];\n\n for (const method of HTTP_METHODS) {\n const patterns = [\n // export async function GET(\n new RegExp(`export\\\\s+(async\\\\s+)?function\\\\s+${method}\\\\s*\\\\(`),\n // export const GET =\n new RegExp(`export\\\\s+const\\\\s+${method}\\\\s*=`),\n // export { handler as GET }\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\bas\\\\s+${method}\\\\b[^}]*\\\\}`),\n ];\n\n if (patterns.some((p) => p.test(content))) {\n methods.push(method);\n }\n }\n\n return methods;\n}\n\n/**\n * Extract path parameters from a Next.js dynamic route path.\n * e.g., \"/api/projects/[id]/tasks/[taskId]\" → [\"id\", \"taskId\"]\n */\nfunction extractPathParams(routePath: string): string[] {\n const params: string[] = [];\n const matches = routePath.matchAll(/\\[([^\\]]+)\\]/g);\n for (const match of matches) {\n params.push(match[1]);\n }\n return params;\n}\n\n/**\n * Clean up path by removing route groups like (group) and catch-all segments.\n * Converts Next.js bracket params to colon params for readability.\n * e.g., \"/api/(admin)/users/[id]\" → \"/api/users/:id\"\n */\nfunction cleanPath(routePath: string): string {\n return routePath\n .replace(/\\/\\([^)]+\\)/g, \"\") // Remove route groups\n .replace(/\\[\\.\\.\\.(\\w+)\\]/g, \":$1*\") // Catch-all [...slug] → :slug*\n .replace(/\\[(\\w+)\\]/g, \":$1\"); // Dynamic [id] → :id\n}\n","import picomatch from \"picomatch\";\nimport type { DiscoveredRoute, DiscoveryConfig } from \"@ai-me-chat/core\";\n\n/**\n * Filter discovered routes based on include/exclude glob patterns.\n * - If include is specified, only routes matching at least one include pattern are kept.\n * - If exclude is specified, routes matching any exclude pattern are removed.\n * - Exclude takes precedence over include.\n */\nexport function filterRoutes(\n routes: DiscoveredRoute[],\n config: Pick<DiscoveryConfig, \"include\" | \"exclude\">,\n): DiscoveredRoute[] {\n let filtered = routes;\n\n if (config.include && config.include.length > 0) {\n const isIncluded = picomatch(config.include);\n filtered = filtered.filter((r) => isIncluded(r.path));\n }\n\n if (config.exclude && config.exclude.length > 0) {\n const isExcluded = picomatch(config.exclude);\n filtered = filtered.filter((r) => !isExcluded(r.path));\n }\n\n return filtered;\n}\n","import { streamText, convertToModelMessages, tool, stepCountIs } from \"ai\";\nimport type { UIMessage } from \"ai\";\nimport {\n type AIMeConfig,\n type AIMeToolDefinition,\n generateToolDefinitions,\n generateToolsFromOpenAPI,\n fetchOpenAPISpec,\n executeTool,\n} from \"@ai-me-chat/core\";\nimport type { OpenAPISpec, ExecutionContext } from \"@ai-me-chat/core\";\nimport { scanRoutes } from \"./scanner.js\";\nimport { filterRoutes } from \"./filter.js\";\n\n/**\n * Create an AI-Me API route handler for Next.js App Router.\n *\n * Usage:\n * const handler = createAIMeHandler({ model, discovery, getSession });\n * export { handler as GET, handler as POST };\n */\nexport function createAIMeHandler(config: AIMeConfig) {\n // Discover tools at initialization time\n let toolDefinitions: AIMeToolDefinition[] | null = null;\n let toolsPromise: Promise<AIMeToolDefinition[]> | null = null;\n\n async function getToolDefinitions(appDir: string): Promise<AIMeToolDefinition[]> {\n if (toolDefinitions) return toolDefinitions;\n if (toolsPromise) return toolsPromise;\n\n toolsPromise = initTools(appDir);\n toolDefinitions = await toolsPromise;\n toolsPromise = null;\n return toolDefinitions;\n }\n\n async function initTools(appDir: string): Promise<AIMeToolDefinition[]> {\n if (config.discovery.mode === \"openapi\") {\n let spec: OpenAPISpec;\n if (config.discovery.spec) {\n spec = config.discovery.spec as unknown as OpenAPISpec;\n } else if (config.discovery.specUrl) {\n spec = await fetchOpenAPISpec(config.discovery.specUrl);\n } else {\n throw new Error(\n 'OpenAPI discovery mode requires either \"spec\" (inline object) or \"specUrl\" (remote URL) in discovery config',\n );\n }\n const tools = generateToolsFromOpenAPI(spec, config.confirmation);\n if (config.discovery.include || config.discovery.exclude) {\n const routes = tools.map((t) => ({\n path: t.path ?? \"\",\n methods: [t.httpMethod ?? \"GET\"],\n pathParams: [],\n filePath: \"\",\n }));\n const filtered = filterRoutes(routes, config.discovery);\n const filteredPaths = new Set(\n filtered.flatMap((r) => r.methods.map((m) => `${m}:${r.path}`)),\n );\n return tools.filter((t) => filteredPaths.has(`${t.httpMethod}:${t.path}`));\n }\n return tools;\n }\n\n let routes = scanRoutes(appDir);\n routes = filterRoutes(routes, config.discovery);\n return generateToolDefinitions(routes, config.confirmation);\n }\n\n async function handler(req: Request): Promise<Response> {\n // Auth check\n const session = await config.getSession(req);\n if (!session) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n\n const url = new URL(req.url);\n\n // Route: GET /api/ai-me/tools — list available tools (debug)\n if (req.method === \"GET\" && url.pathname.endsWith(\"/tools\")) {\n const appDir = process.cwd() + \"/app\";\n let tools: AIMeToolDefinition[];\n try {\n tools = await getToolDefinitions(appDir);\n } catch (error) {\n return new Response(\n JSON.stringify({ error: error instanceof Error ? error.message : \"Tool discovery failed\" }),\n { status: 500, headers: { \"Content-Type\": \"application/json\" } },\n );\n }\n return Response.json(\n tools.map((t) => ({\n name: t.name,\n description: t.description,\n httpMethod: t.httpMethod,\n path: t.path,\n requiresConfirmation: t.requiresConfirmation,\n })),\n );\n }\n\n // Route: GET /api/ai-me/health\n if (req.method === \"GET\" && url.pathname.endsWith(\"/health\")) {\n return Response.json({ status: \"ok\", version: \"0.0.1\" });\n }\n\n // Route: POST /api/ai-me (chat)\n if (req.method === \"POST\") {\n return handleChat(req, config, session, getToolDefinitions);\n }\n\n return new Response(\"Not Found\", { status: 404 });\n }\n\n return handler;\n}\n\nasync function handleChat(\n req: Request,\n config: AIMeConfig,\n session: NonNullable<Awaited<ReturnType<AIMeConfig[\"getSession\"]>>>,\n getToolDefinitions: (appDir: string) => Promise<AIMeToolDefinition[]>,\n): Promise<Response> {\n const { messages }: { messages: UIMessage[] } = await req.json();\n const appDir = process.cwd() + \"/app\";\n const toolDefs = await getToolDefinitions(appDir);\n\n // Build execution context with forwarded auth headers\n const baseUrl = new URL(req.url).origin;\n const executionContext: ExecutionContext = {\n baseUrl,\n headers: {\n cookie: req.headers.get(\"cookie\") ?? \"\",\n authorization: req.headers.get(\"authorization\") ?? \"\",\n },\n };\n\n // Convert tool definitions to AI SDK v6 tool() format\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const aiTools: Record<string, any> = {};\n for (const toolDef of toolDefs) {\n const def = toolDef; // capture for closure\n aiTools[def.name] = tool({\n description: def.description,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n inputSchema: def.parameters as any,\n // Disable strict schema validation for OpenAI-compatible providers\n // (e.g., Groq) that reject additionalProperties in tool schemas\n strict: false,\n execute: async (params: Record<string, unknown>) => {\n const result = await executeTool(\n def,\n params,\n executionContext,\n );\n return result.response;\n },\n });\n }\n\n const modelMessages = await convertToModelMessages(messages);\n\n // Limit history if configured\n const maxHistory = config.maxHistoryMessages ?? 20;\n const trimmedMessages =\n modelMessages.length > maxHistory\n ? modelMessages.slice(-maxHistory)\n : modelMessages;\n\n const result = streamText({\n model: config.model,\n system:\n config.systemPrompt ??\n `You are an AI assistant for this application. You can help users query data and perform actions. User: ${session.user.id}${session.user.role ? ` (role: ${session.user.role})` : \"\"}`,\n messages: trimmedMessages,\n tools: aiTools,\n stopWhen: stepCountIs(5),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(config.providerOptions ? { providerOptions: config.providerOptions as any } : {}),\n });\n\n return result.toUIMessageStreamResponse();\n}\n"],"mappings":"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","names":["picomatch","routes","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/scanner.ts","../src/filter.ts","../src/handler.ts"],"sourcesContent":["export { scanRoutes } from \"./scanner.js\";\nexport { filterRoutes } from \"./filter.js\";\nexport { createAIMeHandler } from \"./handler.js\";\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { DiscoveredRoute } from \"@ai-me-chat/core\";\n\nconst HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\n\nconst ROUTE_FILE_NAMES = [\"route.ts\", \"route.js\", \"route.tsx\", \"route.jsx\"];\n\n/**\n * Scan a Next.js App Router directory for API routes.\n * Finds all route.ts/route.js files under `appDir/api/` and extracts\n * HTTP methods and path parameters.\n */\nexport function scanRoutes(appDir: string): DiscoveredRoute[] {\n const apiDir = path.join(appDir, \"api\");\n if (!fs.existsSync(apiDir)) {\n return [];\n }\n const routes: DiscoveredRoute[] = [];\n walkDirectory(apiDir, appDir, routes);\n return routes.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nfunction walkDirectory(\n dir: string,\n appDir: string,\n routes: DiscoveredRoute[],\n): void {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walkDirectory(fullPath, appDir, routes);\n } else if (ROUTE_FILE_NAMES.includes(entry.name)) {\n const route = parseRouteFile(fullPath, appDir);\n if (route && route.methods.length > 0) {\n routes.push(route);\n }\n }\n }\n}\n\nfunction parseRouteFile(filePath: string, appDir: string): DiscoveredRoute | null {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const methods = extractHttpMethods(content);\n\n if (methods.length === 0) {\n return null;\n }\n\n const relativePath = path.relative(appDir, path.dirname(filePath));\n const apiPath = \"/\" + relativePath.split(path.sep).join(\"/\");\n const pathParams = extractPathParams(apiPath);\n\n return {\n path: cleanPath(apiPath),\n methods,\n pathParams,\n filePath: path.relative(path.resolve(appDir, \"..\"), filePath),\n };\n}\n\n/**\n * Extract exported HTTP method handlers from route file content.\n * Matches patterns like:\n * export async function GET(...)\n * export function POST(...)\n * export const PUT = ...\n * export { handler as DELETE }\n */\nfunction extractHttpMethods(content: string): string[] {\n const methods: string[] = [];\n\n for (const method of HTTP_METHODS) {\n const patterns = [\n // export async function GET(\n new RegExp(`export\\\\s+(async\\\\s+)?function\\\\s+${method}\\\\s*\\\\(`),\n // export const GET =\n new RegExp(`export\\\\s+const\\\\s+${method}\\\\s*=`),\n // export { handler as GET }\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\bas\\\\s+${method}\\\\b[^}]*\\\\}`),\n ];\n\n if (patterns.some((p) => p.test(content))) {\n methods.push(method);\n }\n }\n\n return methods;\n}\n\n/**\n * Extract path parameters from a Next.js dynamic route path.\n * e.g., \"/api/projects/[id]/tasks/[taskId]\" → [\"id\", \"taskId\"]\n */\nfunction extractPathParams(routePath: string): string[] {\n const params: string[] = [];\n const matches = routePath.matchAll(/\\[([^\\]]+)\\]/g);\n for (const match of matches) {\n params.push(match[1]);\n }\n return params;\n}\n\n/**\n * Clean up path by removing route groups like (group) and catch-all segments.\n * Converts Next.js bracket params to colon params for readability.\n * e.g., \"/api/(admin)/users/[id]\" → \"/api/users/:id\"\n */\nfunction cleanPath(routePath: string): string {\n return routePath\n .replace(/\\/\\([^)]+\\)/g, \"\") // Remove route groups\n .replace(/\\[\\.\\.\\.(\\w+)\\]/g, \":$1*\") // Catch-all [...slug] → :slug*\n .replace(/\\[(\\w+)\\]/g, \":$1\"); // Dynamic [id] → :id\n}\n","import picomatch from \"picomatch\";\nimport type { DiscoveredRoute, DiscoveryConfig } from \"@ai-me-chat/core\";\n\n/**\n * Filter discovered routes based on include/exclude glob patterns.\n * - If include is specified, only routes matching at least one include pattern are kept.\n * - If exclude is specified, routes matching any exclude pattern are removed.\n * - Exclude takes precedence over include.\n */\nexport function filterRoutes(\n routes: DiscoveredRoute[],\n config: Pick<DiscoveryConfig, \"include\" | \"exclude\">,\n): DiscoveredRoute[] {\n let filtered = routes;\n\n if (config.include && config.include.length > 0) {\n const isIncluded = picomatch(config.include);\n filtered = filtered.filter((r) => isIncluded(r.path));\n }\n\n if (config.exclude && config.exclude.length > 0) {\n const isExcluded = picomatch(config.exclude);\n filtered = filtered.filter((r) => !isExcluded(r.path));\n }\n\n return filtered;\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { streamText, convertToModelMessages, tool, stepCountIs } from \"ai\";\nimport type { UIMessage } from \"ai\";\nimport {\n type AIMeConfig,\n type AIMeToolDefinition,\n generateToolDefinitions,\n generateToolsFromOpenAPI,\n fetchOpenAPISpec,\n executeTool,\n} from \"@ai-me-chat/core\";\nimport type { OpenAPISpec, ExecutionContext } from \"@ai-me-chat/core\";\nimport { scanRoutes } from \"./scanner.js\";\nimport { filterRoutes } from \"./filter.js\";\n\n/**\n * Resolve the Next.js app directory.\n *\n * Priority:\n * 1. `config.discovery.appDir` — explicit override (absolute or relative to cwd)\n * 2. `src/app` — default for `create-next-app` projects\n * 3. `app` — legacy / bare Next.js layout\n */\nexport function detectAppDir(): string {\n const srcApp = path.join(process.cwd(), \"src\", \"app\");\n if (fs.existsSync(srcApp)) return srcApp;\n return path.join(process.cwd(), \"app\");\n}\n\nexport function resolveAppDir(config: AIMeConfig): string {\n if (config.discovery.appDir) {\n return path.resolve(process.cwd(), config.discovery.appDir);\n }\n return detectAppDir();\n}\n\n/**\n * Create an AI-Me API route handler for Next.js App Router.\n *\n * Usage:\n * const handler = createAIMeHandler({ model, discovery, getSession });\n * export { handler as GET, handler as POST };\n */\nexport function createAIMeHandler(config: AIMeConfig) {\n // Resolve the app directory once at handler-creation time so both the\n // /tools endpoint and handleChat use the same value.\n const appDir = resolveAppDir(config);\n\n // Discover tools at initialization time\n let toolDefinitions: AIMeToolDefinition[] | null = null;\n let toolsPromise: Promise<AIMeToolDefinition[]> | null = null;\n\n async function getToolDefinitions(): Promise<AIMeToolDefinition[]> {\n if (toolDefinitions) return toolDefinitions;\n if (toolsPromise) return toolsPromise;\n\n toolsPromise = initTools();\n toolDefinitions = await toolsPromise;\n toolsPromise = null;\n return toolDefinitions;\n }\n\n async function initTools(): Promise<AIMeToolDefinition[]> {\n if (config.discovery.mode === \"openapi\") {\n let spec: OpenAPISpec;\n if (config.discovery.spec) {\n spec = config.discovery.spec as unknown as OpenAPISpec;\n } else if (config.discovery.specUrl) {\n spec = await fetchOpenAPISpec(config.discovery.specUrl);\n } else {\n throw new Error(\n 'OpenAPI discovery mode requires either \"spec\" (inline object) or \"specUrl\" (remote URL) in discovery config',\n );\n }\n const tools = generateToolsFromOpenAPI(spec, config.confirmation);\n if (config.discovery.include || config.discovery.exclude) {\n const routes = tools.map((t) => ({\n path: t.path ?? \"\",\n methods: [t.httpMethod ?? \"GET\"],\n pathParams: [],\n filePath: \"\",\n }));\n const filtered = filterRoutes(routes, config.discovery);\n const filteredPaths = new Set(\n filtered.flatMap((r) => r.methods.map((m) => `${m}:${r.path}`)),\n );\n return tools.filter((t) => filteredPaths.has(`${t.httpMethod}:${t.path}`));\n }\n return tools;\n }\n\n let routes = scanRoutes(appDir);\n routes = filterRoutes(routes, config.discovery);\n return generateToolDefinitions(routes, config.confirmation);\n }\n\n async function handler(req: Request): Promise<Response> {\n const url = new URL(req.url);\n\n // Health check — always public, no auth required\n if (req.method === \"GET\" && url.pathname.endsWith(\"/health\")) {\n return Response.json({ status: \"ok\" });\n }\n\n // Auth check — everything else requires a session\n const session = await config.getSession(req);\n if (!session) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n\n // Route: GET /api/ai-me/tools — list available tools (debug)\n if (req.method === \"GET\" && url.pathname.endsWith(\"/tools\")) {\n let tools: AIMeToolDefinition[];\n try {\n tools = await getToolDefinitions();\n } catch (error) {\n return Response.json(\n { error: error instanceof Error ? error.message : \"Tool discovery failed\" },\n { status: 500 },\n );\n }\n return Response.json(\n tools.map((t) => ({\n name: t.name,\n description: t.description,\n httpMethod: t.httpMethod,\n path: t.path,\n requiresConfirmation: t.requiresConfirmation,\n })),\n );\n }\n\n // Route: POST /api/ai-me (chat)\n if (req.method === \"POST\") {\n return handleChat(req, config, session, getToolDefinitions, url.origin);\n }\n\n return new Response(\"Not Found\", { status: 404 });\n }\n\n return handler;\n}\n\nasync function handleChat(\n req: Request,\n config: AIMeConfig,\n session: NonNullable<Awaited<ReturnType<AIMeConfig[\"getSession\"]>>>,\n getToolDefinitions: () => Promise<AIMeToolDefinition[]>,\n baseUrl: string,\n): Promise<Response> {\n // Parse and validate request body\n let body: { messages?: unknown };\n try {\n body = await req.json();\n } catch {\n return Response.json({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n\n const { messages } = body;\n if (!Array.isArray(messages) || messages.length === 0) {\n return Response.json({ error: \"messages must be a non-empty array\" }, { status: 400 });\n }\n\n // Validate each message has required UIMessage fields\n for (const msg of messages) {\n if (!msg.id || !msg.role) {\n return Response.json(\n {\n error: \"Each message must have an 'id' and 'role' field. Use the AI SDK UIMessage format.\",\n received: Object.keys(msg),\n },\n { status: 400 },\n );\n }\n }\n\n const toolDefs = await getToolDefinitions();\n const executionContext: ExecutionContext = {\n baseUrl,\n headers: {\n cookie: req.headers.get(\"cookie\") ?? \"\",\n authorization: req.headers.get(\"authorization\") ?? \"\",\n },\n };\n\n // Convert tool definitions to AI SDK v6 tool() format\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const aiTools: Record<string, any> = {};\n for (const toolDef of toolDefs) {\n aiTools[toolDef.name] = tool({\n description: toolDef.description,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n inputSchema: toolDef.parameters as any,\n // Disable strict schema validation for OpenAI-compatible providers\n // (e.g., Groq) that reject additionalProperties in tool schemas\n strict: false,\n execute: async (params: Record<string, unknown>) => {\n const result = await executeTool(\n toolDef,\n params,\n executionContext,\n );\n return result.response;\n },\n });\n }\n\n const modelMessages = await convertToModelMessages(messages as UIMessage[]);\n\n // Limit history if configured\n const maxHistory = config.maxHistoryMessages ?? 20;\n const trimmedMessages =\n modelMessages.length > maxHistory\n ? modelMessages.slice(-maxHistory)\n : modelMessages;\n\n // Resolve system prompt — supports static string or async function\n const defaultPrompt = `You are an AI assistant for this application. You can help users query data and perform actions. User: ${session.user.id}${session.user.role ? ` (role: ${session.user.role})` : \"\"}`;\n const systemPromptValue =\n typeof config.systemPrompt === \"function\"\n ? await config.systemPrompt(session)\n : config.systemPrompt ?? defaultPrompt;\n\n const result = streamText({\n model: config.model,\n system: systemPromptValue,\n messages: trimmedMessages,\n tools: aiTools,\n stopWhen: stepCountIs(5),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(config.providerOptions ? { providerOptions: config.providerOptions as any } : {}),\n });\n\n return result.toUIMessageStreamResponse();\n}\n"],"mappings":"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","names":["picomatch","path","fs","routes","result"]}
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,8 @@ function filterRoutes(routes, config) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// src/handler.ts
|
|
90
|
+
import fs2 from "fs";
|
|
91
|
+
import path2 from "path";
|
|
90
92
|
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
|
|
91
93
|
import {
|
|
92
94
|
generateToolDefinitions,
|
|
@@ -94,18 +96,30 @@ import {
|
|
|
94
96
|
fetchOpenAPISpec,
|
|
95
97
|
executeTool
|
|
96
98
|
} from "@ai-me-chat/core";
|
|
99
|
+
function detectAppDir() {
|
|
100
|
+
const srcApp = path2.join(process.cwd(), "src", "app");
|
|
101
|
+
if (fs2.existsSync(srcApp)) return srcApp;
|
|
102
|
+
return path2.join(process.cwd(), "app");
|
|
103
|
+
}
|
|
104
|
+
function resolveAppDir(config) {
|
|
105
|
+
if (config.discovery.appDir) {
|
|
106
|
+
return path2.resolve(process.cwd(), config.discovery.appDir);
|
|
107
|
+
}
|
|
108
|
+
return detectAppDir();
|
|
109
|
+
}
|
|
97
110
|
function createAIMeHandler(config) {
|
|
111
|
+
const appDir = resolveAppDir(config);
|
|
98
112
|
let toolDefinitions = null;
|
|
99
113
|
let toolsPromise = null;
|
|
100
|
-
async function getToolDefinitions(
|
|
114
|
+
async function getToolDefinitions() {
|
|
101
115
|
if (toolDefinitions) return toolDefinitions;
|
|
102
116
|
if (toolsPromise) return toolsPromise;
|
|
103
|
-
toolsPromise = initTools(
|
|
117
|
+
toolsPromise = initTools();
|
|
104
118
|
toolDefinitions = await toolsPromise;
|
|
105
119
|
toolsPromise = null;
|
|
106
120
|
return toolDefinitions;
|
|
107
121
|
}
|
|
108
|
-
async function initTools(
|
|
122
|
+
async function initTools() {
|
|
109
123
|
if (config.discovery.mode === "openapi") {
|
|
110
124
|
let spec;
|
|
111
125
|
if (config.discovery.spec) {
|
|
@@ -138,20 +152,22 @@ function createAIMeHandler(config) {
|
|
|
138
152
|
return generateToolDefinitions(routes, config.confirmation);
|
|
139
153
|
}
|
|
140
154
|
async function handler(req) {
|
|
155
|
+
const url = new URL(req.url);
|
|
156
|
+
if (req.method === "GET" && url.pathname.endsWith("/health")) {
|
|
157
|
+
return Response.json({ status: "ok" });
|
|
158
|
+
}
|
|
141
159
|
const session = await config.getSession(req);
|
|
142
160
|
if (!session) {
|
|
143
161
|
return new Response("Unauthorized", { status: 401 });
|
|
144
162
|
}
|
|
145
|
-
const url = new URL(req.url);
|
|
146
163
|
if (req.method === "GET" && url.pathname.endsWith("/tools")) {
|
|
147
|
-
const appDir = process.cwd() + "/app";
|
|
148
164
|
let tools;
|
|
149
165
|
try {
|
|
150
|
-
tools = await getToolDefinitions(
|
|
166
|
+
tools = await getToolDefinitions();
|
|
151
167
|
} catch (error) {
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
{ status: 500
|
|
168
|
+
return Response.json(
|
|
169
|
+
{ error: error instanceof Error ? error.message : "Tool discovery failed" },
|
|
170
|
+
{ status: 500 }
|
|
155
171
|
);
|
|
156
172
|
}
|
|
157
173
|
return Response.json(
|
|
@@ -164,21 +180,36 @@ function createAIMeHandler(config) {
|
|
|
164
180
|
}))
|
|
165
181
|
);
|
|
166
182
|
}
|
|
167
|
-
if (req.method === "GET" && url.pathname.endsWith("/health")) {
|
|
168
|
-
return Response.json({ status: "ok", version: "0.0.1" });
|
|
169
|
-
}
|
|
170
183
|
if (req.method === "POST") {
|
|
171
|
-
return handleChat(req, config, session, getToolDefinitions);
|
|
184
|
+
return handleChat(req, config, session, getToolDefinitions, url.origin);
|
|
172
185
|
}
|
|
173
186
|
return new Response("Not Found", { status: 404 });
|
|
174
187
|
}
|
|
175
188
|
return handler;
|
|
176
189
|
}
|
|
177
|
-
async function handleChat(req, config, session, getToolDefinitions) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
async function handleChat(req, config, session, getToolDefinitions, baseUrl) {
|
|
191
|
+
let body;
|
|
192
|
+
try {
|
|
193
|
+
body = await req.json();
|
|
194
|
+
} catch {
|
|
195
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
196
|
+
}
|
|
197
|
+
const { messages } = body;
|
|
198
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
199
|
+
return Response.json({ error: "messages must be a non-empty array" }, { status: 400 });
|
|
200
|
+
}
|
|
201
|
+
for (const msg of messages) {
|
|
202
|
+
if (!msg.id || !msg.role) {
|
|
203
|
+
return Response.json(
|
|
204
|
+
{
|
|
205
|
+
error: "Each message must have an 'id' and 'role' field. Use the AI SDK UIMessage format.",
|
|
206
|
+
received: Object.keys(msg)
|
|
207
|
+
},
|
|
208
|
+
{ status: 400 }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const toolDefs = await getToolDefinitions();
|
|
182
213
|
const executionContext = {
|
|
183
214
|
baseUrl,
|
|
184
215
|
headers: {
|
|
@@ -188,17 +219,16 @@ async function handleChat(req, config, session, getToolDefinitions) {
|
|
|
188
219
|
};
|
|
189
220
|
const aiTools = {};
|
|
190
221
|
for (const toolDef of toolDefs) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
description: def.description,
|
|
222
|
+
aiTools[toolDef.name] = tool({
|
|
223
|
+
description: toolDef.description,
|
|
194
224
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
195
|
-
inputSchema:
|
|
225
|
+
inputSchema: toolDef.parameters,
|
|
196
226
|
// Disable strict schema validation for OpenAI-compatible providers
|
|
197
227
|
// (e.g., Groq) that reject additionalProperties in tool schemas
|
|
198
228
|
strict: false,
|
|
199
229
|
execute: async (params) => {
|
|
200
230
|
const result2 = await executeTool(
|
|
201
|
-
|
|
231
|
+
toolDef,
|
|
202
232
|
params,
|
|
203
233
|
executionContext
|
|
204
234
|
);
|
|
@@ -209,9 +239,11 @@ async function handleChat(req, config, session, getToolDefinitions) {
|
|
|
209
239
|
const modelMessages = await convertToModelMessages(messages);
|
|
210
240
|
const maxHistory = config.maxHistoryMessages ?? 20;
|
|
211
241
|
const trimmedMessages = modelMessages.length > maxHistory ? modelMessages.slice(-maxHistory) : modelMessages;
|
|
242
|
+
const defaultPrompt = `You are an AI assistant for this application. You can help users query data and perform actions. User: ${session.user.id}${session.user.role ? ` (role: ${session.user.role})` : ""}`;
|
|
243
|
+
const systemPromptValue = typeof config.systemPrompt === "function" ? await config.systemPrompt(session) : config.systemPrompt ?? defaultPrompt;
|
|
212
244
|
const result = streamText({
|
|
213
245
|
model: config.model,
|
|
214
|
-
system:
|
|
246
|
+
system: systemPromptValue,
|
|
215
247
|
messages: trimmedMessages,
|
|
216
248
|
tools: aiTools,
|
|
217
249
|
stopWhen: stepCountIs(5),
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/scanner.ts","../src/filter.ts","../src/handler.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { DiscoveredRoute } from \"@ai-me-chat/core\";\n\nconst HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\n\nconst ROUTE_FILE_NAMES = [\"route.ts\", \"route.js\", \"route.tsx\", \"route.jsx\"];\n\n/**\n * Scan a Next.js App Router directory for API routes.\n * Finds all route.ts/route.js files under `appDir/api/` and extracts\n * HTTP methods and path parameters.\n */\nexport function scanRoutes(appDir: string): DiscoveredRoute[] {\n const apiDir = path.join(appDir, \"api\");\n if (!fs.existsSync(apiDir)) {\n return [];\n }\n const routes: DiscoveredRoute[] = [];\n walkDirectory(apiDir, appDir, routes);\n return routes.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nfunction walkDirectory(\n dir: string,\n appDir: string,\n routes: DiscoveredRoute[],\n): void {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walkDirectory(fullPath, appDir, routes);\n } else if (ROUTE_FILE_NAMES.includes(entry.name)) {\n const route = parseRouteFile(fullPath, appDir);\n if (route && route.methods.length > 0) {\n routes.push(route);\n }\n }\n }\n}\n\nfunction parseRouteFile(filePath: string, appDir: string): DiscoveredRoute | null {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const methods = extractHttpMethods(content);\n\n if (methods.length === 0) {\n return null;\n }\n\n const relativePath = path.relative(appDir, path.dirname(filePath));\n const apiPath = \"/\" + relativePath.split(path.sep).join(\"/\");\n const pathParams = extractPathParams(apiPath);\n\n return {\n path: cleanPath(apiPath),\n methods,\n pathParams,\n filePath: path.relative(path.resolve(appDir, \"..\"), filePath),\n };\n}\n\n/**\n * Extract exported HTTP method handlers from route file content.\n * Matches patterns like:\n * export async function GET(...)\n * export function POST(...)\n * export const PUT = ...\n * export { handler as DELETE }\n */\nfunction extractHttpMethods(content: string): string[] {\n const methods: string[] = [];\n\n for (const method of HTTP_METHODS) {\n const patterns = [\n // export async function GET(\n new RegExp(`export\\\\s+(async\\\\s+)?function\\\\s+${method}\\\\s*\\\\(`),\n // export const GET =\n new RegExp(`export\\\\s+const\\\\s+${method}\\\\s*=`),\n // export { handler as GET }\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\bas\\\\s+${method}\\\\b[^}]*\\\\}`),\n ];\n\n if (patterns.some((p) => p.test(content))) {\n methods.push(method);\n }\n }\n\n return methods;\n}\n\n/**\n * Extract path parameters from a Next.js dynamic route path.\n * e.g., \"/api/projects/[id]/tasks/[taskId]\" → [\"id\", \"taskId\"]\n */\nfunction extractPathParams(routePath: string): string[] {\n const params: string[] = [];\n const matches = routePath.matchAll(/\\[([^\\]]+)\\]/g);\n for (const match of matches) {\n params.push(match[1]);\n }\n return params;\n}\n\n/**\n * Clean up path by removing route groups like (group) and catch-all segments.\n * Converts Next.js bracket params to colon params for readability.\n * e.g., \"/api/(admin)/users/[id]\" → \"/api/users/:id\"\n */\nfunction cleanPath(routePath: string): string {\n return routePath\n .replace(/\\/\\([^)]+\\)/g, \"\") // Remove route groups\n .replace(/\\[\\.\\.\\.(\\w+)\\]/g, \":$1*\") // Catch-all [...slug] → :slug*\n .replace(/\\[(\\w+)\\]/g, \":$1\"); // Dynamic [id] → :id\n}\n","import picomatch from \"picomatch\";\nimport type { DiscoveredRoute, DiscoveryConfig } from \"@ai-me-chat/core\";\n\n/**\n * Filter discovered routes based on include/exclude glob patterns.\n * - If include is specified, only routes matching at least one include pattern are kept.\n * - If exclude is specified, routes matching any exclude pattern are removed.\n * - Exclude takes precedence over include.\n */\nexport function filterRoutes(\n routes: DiscoveredRoute[],\n config: Pick<DiscoveryConfig, \"include\" | \"exclude\">,\n): DiscoveredRoute[] {\n let filtered = routes;\n\n if (config.include && config.include.length > 0) {\n const isIncluded = picomatch(config.include);\n filtered = filtered.filter((r) => isIncluded(r.path));\n }\n\n if (config.exclude && config.exclude.length > 0) {\n const isExcluded = picomatch(config.exclude);\n filtered = filtered.filter((r) => !isExcluded(r.path));\n }\n\n return filtered;\n}\n","import { streamText, convertToModelMessages, tool, stepCountIs } from \"ai\";\nimport type { UIMessage } from \"ai\";\nimport {\n type AIMeConfig,\n type AIMeToolDefinition,\n generateToolDefinitions,\n generateToolsFromOpenAPI,\n fetchOpenAPISpec,\n executeTool,\n} from \"@ai-me-chat/core\";\nimport type { OpenAPISpec, ExecutionContext } from \"@ai-me-chat/core\";\nimport { scanRoutes } from \"./scanner.js\";\nimport { filterRoutes } from \"./filter.js\";\n\n/**\n * Create an AI-Me API route handler for Next.js App Router.\n *\n * Usage:\n * const handler = createAIMeHandler({ model, discovery, getSession });\n * export { handler as GET, handler as POST };\n */\nexport function createAIMeHandler(config: AIMeConfig) {\n // Discover tools at initialization time\n let toolDefinitions: AIMeToolDefinition[] | null = null;\n let toolsPromise: Promise<AIMeToolDefinition[]> | null = null;\n\n async function getToolDefinitions(appDir: string): Promise<AIMeToolDefinition[]> {\n if (toolDefinitions) return toolDefinitions;\n if (toolsPromise) return toolsPromise;\n\n toolsPromise = initTools(appDir);\n toolDefinitions = await toolsPromise;\n toolsPromise = null;\n return toolDefinitions;\n }\n\n async function initTools(appDir: string): Promise<AIMeToolDefinition[]> {\n if (config.discovery.mode === \"openapi\") {\n let spec: OpenAPISpec;\n if (config.discovery.spec) {\n spec = config.discovery.spec as unknown as OpenAPISpec;\n } else if (config.discovery.specUrl) {\n spec = await fetchOpenAPISpec(config.discovery.specUrl);\n } else {\n throw new Error(\n 'OpenAPI discovery mode requires either \"spec\" (inline object) or \"specUrl\" (remote URL) in discovery config',\n );\n }\n const tools = generateToolsFromOpenAPI(spec, config.confirmation);\n if (config.discovery.include || config.discovery.exclude) {\n const routes = tools.map((t) => ({\n path: t.path ?? \"\",\n methods: [t.httpMethod ?? \"GET\"],\n pathParams: [],\n filePath: \"\",\n }));\n const filtered = filterRoutes(routes, config.discovery);\n const filteredPaths = new Set(\n filtered.flatMap((r) => r.methods.map((m) => `${m}:${r.path}`)),\n );\n return tools.filter((t) => filteredPaths.has(`${t.httpMethod}:${t.path}`));\n }\n return tools;\n }\n\n let routes = scanRoutes(appDir);\n routes = filterRoutes(routes, config.discovery);\n return generateToolDefinitions(routes, config.confirmation);\n }\n\n async function handler(req: Request): Promise<Response> {\n // Auth check\n const session = await config.getSession(req);\n if (!session) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n\n const url = new URL(req.url);\n\n // Route: GET /api/ai-me/tools — list available tools (debug)\n if (req.method === \"GET\" && url.pathname.endsWith(\"/tools\")) {\n const appDir = process.cwd() + \"/app\";\n let tools: AIMeToolDefinition[];\n try {\n tools = await getToolDefinitions(appDir);\n } catch (error) {\n return new Response(\n JSON.stringify({ error: error instanceof Error ? error.message : \"Tool discovery failed\" }),\n { status: 500, headers: { \"Content-Type\": \"application/json\" } },\n );\n }\n return Response.json(\n tools.map((t) => ({\n name: t.name,\n description: t.description,\n httpMethod: t.httpMethod,\n path: t.path,\n requiresConfirmation: t.requiresConfirmation,\n })),\n );\n }\n\n // Route: GET /api/ai-me/health\n if (req.method === \"GET\" && url.pathname.endsWith(\"/health\")) {\n return Response.json({ status: \"ok\", version: \"0.0.1\" });\n }\n\n // Route: POST /api/ai-me (chat)\n if (req.method === \"POST\") {\n return handleChat(req, config, session, getToolDefinitions);\n }\n\n return new Response(\"Not Found\", { status: 404 });\n }\n\n return handler;\n}\n\nasync function handleChat(\n req: Request,\n config: AIMeConfig,\n session: NonNullable<Awaited<ReturnType<AIMeConfig[\"getSession\"]>>>,\n getToolDefinitions: (appDir: string) => Promise<AIMeToolDefinition[]>,\n): Promise<Response> {\n const { messages }: { messages: UIMessage[] } = await req.json();\n const appDir = process.cwd() + \"/app\";\n const toolDefs = await getToolDefinitions(appDir);\n\n // Build execution context with forwarded auth headers\n const baseUrl = new URL(req.url).origin;\n const executionContext: ExecutionContext = {\n baseUrl,\n headers: {\n cookie: req.headers.get(\"cookie\") ?? \"\",\n authorization: req.headers.get(\"authorization\") ?? \"\",\n },\n };\n\n // Convert tool definitions to AI SDK v6 tool() format\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const aiTools: Record<string, any> = {};\n for (const toolDef of toolDefs) {\n const def = toolDef; // capture for closure\n aiTools[def.name] = tool({\n description: def.description,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n inputSchema: def.parameters as any,\n // Disable strict schema validation for OpenAI-compatible providers\n // (e.g., Groq) that reject additionalProperties in tool schemas\n strict: false,\n execute: async (params: Record<string, unknown>) => {\n const result = await executeTool(\n def,\n params,\n executionContext,\n );\n return result.response;\n },\n });\n }\n\n const modelMessages = await convertToModelMessages(messages);\n\n // Limit history if configured\n const maxHistory = config.maxHistoryMessages ?? 20;\n const trimmedMessages =\n modelMessages.length > maxHistory\n ? modelMessages.slice(-maxHistory)\n : modelMessages;\n\n const result = streamText({\n model: config.model,\n system:\n config.systemPrompt ??\n `You are an AI assistant for this application. You can help users query data and perform actions. User: ${session.user.id}${session.user.role ? ` (role: ${session.user.role})` : \"\"}`,\n messages: trimmedMessages,\n tools: aiTools,\n stopWhen: stepCountIs(5),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(config.providerOptions ? { providerOptions: config.providerOptions as any } : {}),\n });\n\n return result.toUIMessageStreamResponse();\n}\n"],"mappings":"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","names":["routes","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/scanner.ts","../src/filter.ts","../src/handler.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { DiscoveredRoute } from \"@ai-me-chat/core\";\n\nconst HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\n\nconst ROUTE_FILE_NAMES = [\"route.ts\", \"route.js\", \"route.tsx\", \"route.jsx\"];\n\n/**\n * Scan a Next.js App Router directory for API routes.\n * Finds all route.ts/route.js files under `appDir/api/` and extracts\n * HTTP methods and path parameters.\n */\nexport function scanRoutes(appDir: string): DiscoveredRoute[] {\n const apiDir = path.join(appDir, \"api\");\n if (!fs.existsSync(apiDir)) {\n return [];\n }\n const routes: DiscoveredRoute[] = [];\n walkDirectory(apiDir, appDir, routes);\n return routes.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nfunction walkDirectory(\n dir: string,\n appDir: string,\n routes: DiscoveredRoute[],\n): void {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walkDirectory(fullPath, appDir, routes);\n } else if (ROUTE_FILE_NAMES.includes(entry.name)) {\n const route = parseRouteFile(fullPath, appDir);\n if (route && route.methods.length > 0) {\n routes.push(route);\n }\n }\n }\n}\n\nfunction parseRouteFile(filePath: string, appDir: string): DiscoveredRoute | null {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const methods = extractHttpMethods(content);\n\n if (methods.length === 0) {\n return null;\n }\n\n const relativePath = path.relative(appDir, path.dirname(filePath));\n const apiPath = \"/\" + relativePath.split(path.sep).join(\"/\");\n const pathParams = extractPathParams(apiPath);\n\n return {\n path: cleanPath(apiPath),\n methods,\n pathParams,\n filePath: path.relative(path.resolve(appDir, \"..\"), filePath),\n };\n}\n\n/**\n * Extract exported HTTP method handlers from route file content.\n * Matches patterns like:\n * export async function GET(...)\n * export function POST(...)\n * export const PUT = ...\n * export { handler as DELETE }\n */\nfunction extractHttpMethods(content: string): string[] {\n const methods: string[] = [];\n\n for (const method of HTTP_METHODS) {\n const patterns = [\n // export async function GET(\n new RegExp(`export\\\\s+(async\\\\s+)?function\\\\s+${method}\\\\s*\\\\(`),\n // export const GET =\n new RegExp(`export\\\\s+const\\\\s+${method}\\\\s*=`),\n // export { handler as GET }\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\bas\\\\s+${method}\\\\b[^}]*\\\\}`),\n ];\n\n if (patterns.some((p) => p.test(content))) {\n methods.push(method);\n }\n }\n\n return methods;\n}\n\n/**\n * Extract path parameters from a Next.js dynamic route path.\n * e.g., \"/api/projects/[id]/tasks/[taskId]\" → [\"id\", \"taskId\"]\n */\nfunction extractPathParams(routePath: string): string[] {\n const params: string[] = [];\n const matches = routePath.matchAll(/\\[([^\\]]+)\\]/g);\n for (const match of matches) {\n params.push(match[1]);\n }\n return params;\n}\n\n/**\n * Clean up path by removing route groups like (group) and catch-all segments.\n * Converts Next.js bracket params to colon params for readability.\n * e.g., \"/api/(admin)/users/[id]\" → \"/api/users/:id\"\n */\nfunction cleanPath(routePath: string): string {\n return routePath\n .replace(/\\/\\([^)]+\\)/g, \"\") // Remove route groups\n .replace(/\\[\\.\\.\\.(\\w+)\\]/g, \":$1*\") // Catch-all [...slug] → :slug*\n .replace(/\\[(\\w+)\\]/g, \":$1\"); // Dynamic [id] → :id\n}\n","import picomatch from \"picomatch\";\nimport type { DiscoveredRoute, DiscoveryConfig } from \"@ai-me-chat/core\";\n\n/**\n * Filter discovered routes based on include/exclude glob patterns.\n * - If include is specified, only routes matching at least one include pattern are kept.\n * - If exclude is specified, routes matching any exclude pattern are removed.\n * - Exclude takes precedence over include.\n */\nexport function filterRoutes(\n routes: DiscoveredRoute[],\n config: Pick<DiscoveryConfig, \"include\" | \"exclude\">,\n): DiscoveredRoute[] {\n let filtered = routes;\n\n if (config.include && config.include.length > 0) {\n const isIncluded = picomatch(config.include);\n filtered = filtered.filter((r) => isIncluded(r.path));\n }\n\n if (config.exclude && config.exclude.length > 0) {\n const isExcluded = picomatch(config.exclude);\n filtered = filtered.filter((r) => !isExcluded(r.path));\n }\n\n return filtered;\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport { streamText, convertToModelMessages, tool, stepCountIs } from \"ai\";\nimport type { UIMessage } from \"ai\";\nimport {\n type AIMeConfig,\n type AIMeToolDefinition,\n generateToolDefinitions,\n generateToolsFromOpenAPI,\n fetchOpenAPISpec,\n executeTool,\n} from \"@ai-me-chat/core\";\nimport type { OpenAPISpec, ExecutionContext } from \"@ai-me-chat/core\";\nimport { scanRoutes } from \"./scanner.js\";\nimport { filterRoutes } from \"./filter.js\";\n\n/**\n * Resolve the Next.js app directory.\n *\n * Priority:\n * 1. `config.discovery.appDir` — explicit override (absolute or relative to cwd)\n * 2. `src/app` — default for `create-next-app` projects\n * 3. `app` — legacy / bare Next.js layout\n */\nexport function detectAppDir(): string {\n const srcApp = path.join(process.cwd(), \"src\", \"app\");\n if (fs.existsSync(srcApp)) return srcApp;\n return path.join(process.cwd(), \"app\");\n}\n\nexport function resolveAppDir(config: AIMeConfig): string {\n if (config.discovery.appDir) {\n return path.resolve(process.cwd(), config.discovery.appDir);\n }\n return detectAppDir();\n}\n\n/**\n * Create an AI-Me API route handler for Next.js App Router.\n *\n * Usage:\n * const handler = createAIMeHandler({ model, discovery, getSession });\n * export { handler as GET, handler as POST };\n */\nexport function createAIMeHandler(config: AIMeConfig) {\n // Resolve the app directory once at handler-creation time so both the\n // /tools endpoint and handleChat use the same value.\n const appDir = resolveAppDir(config);\n\n // Discover tools at initialization time\n let toolDefinitions: AIMeToolDefinition[] | null = null;\n let toolsPromise: Promise<AIMeToolDefinition[]> | null = null;\n\n async function getToolDefinitions(): Promise<AIMeToolDefinition[]> {\n if (toolDefinitions) return toolDefinitions;\n if (toolsPromise) return toolsPromise;\n\n toolsPromise = initTools();\n toolDefinitions = await toolsPromise;\n toolsPromise = null;\n return toolDefinitions;\n }\n\n async function initTools(): Promise<AIMeToolDefinition[]> {\n if (config.discovery.mode === \"openapi\") {\n let spec: OpenAPISpec;\n if (config.discovery.spec) {\n spec = config.discovery.spec as unknown as OpenAPISpec;\n } else if (config.discovery.specUrl) {\n spec = await fetchOpenAPISpec(config.discovery.specUrl);\n } else {\n throw new Error(\n 'OpenAPI discovery mode requires either \"spec\" (inline object) or \"specUrl\" (remote URL) in discovery config',\n );\n }\n const tools = generateToolsFromOpenAPI(spec, config.confirmation);\n if (config.discovery.include || config.discovery.exclude) {\n const routes = tools.map((t) => ({\n path: t.path ?? \"\",\n methods: [t.httpMethod ?? \"GET\"],\n pathParams: [],\n filePath: \"\",\n }));\n const filtered = filterRoutes(routes, config.discovery);\n const filteredPaths = new Set(\n filtered.flatMap((r) => r.methods.map((m) => `${m}:${r.path}`)),\n );\n return tools.filter((t) => filteredPaths.has(`${t.httpMethod}:${t.path}`));\n }\n return tools;\n }\n\n let routes = scanRoutes(appDir);\n routes = filterRoutes(routes, config.discovery);\n return generateToolDefinitions(routes, config.confirmation);\n }\n\n async function handler(req: Request): Promise<Response> {\n const url = new URL(req.url);\n\n // Health check — always public, no auth required\n if (req.method === \"GET\" && url.pathname.endsWith(\"/health\")) {\n return Response.json({ status: \"ok\" });\n }\n\n // Auth check — everything else requires a session\n const session = await config.getSession(req);\n if (!session) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n\n // Route: GET /api/ai-me/tools — list available tools (debug)\n if (req.method === \"GET\" && url.pathname.endsWith(\"/tools\")) {\n let tools: AIMeToolDefinition[];\n try {\n tools = await getToolDefinitions();\n } catch (error) {\n return Response.json(\n { error: error instanceof Error ? error.message : \"Tool discovery failed\" },\n { status: 500 },\n );\n }\n return Response.json(\n tools.map((t) => ({\n name: t.name,\n description: t.description,\n httpMethod: t.httpMethod,\n path: t.path,\n requiresConfirmation: t.requiresConfirmation,\n })),\n );\n }\n\n // Route: POST /api/ai-me (chat)\n if (req.method === \"POST\") {\n return handleChat(req, config, session, getToolDefinitions, url.origin);\n }\n\n return new Response(\"Not Found\", { status: 404 });\n }\n\n return handler;\n}\n\nasync function handleChat(\n req: Request,\n config: AIMeConfig,\n session: NonNullable<Awaited<ReturnType<AIMeConfig[\"getSession\"]>>>,\n getToolDefinitions: () => Promise<AIMeToolDefinition[]>,\n baseUrl: string,\n): Promise<Response> {\n // Parse and validate request body\n let body: { messages?: unknown };\n try {\n body = await req.json();\n } catch {\n return Response.json({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n\n const { messages } = body;\n if (!Array.isArray(messages) || messages.length === 0) {\n return Response.json({ error: \"messages must be a non-empty array\" }, { status: 400 });\n }\n\n // Validate each message has required UIMessage fields\n for (const msg of messages) {\n if (!msg.id || !msg.role) {\n return Response.json(\n {\n error: \"Each message must have an 'id' and 'role' field. Use the AI SDK UIMessage format.\",\n received: Object.keys(msg),\n },\n { status: 400 },\n );\n }\n }\n\n const toolDefs = await getToolDefinitions();\n const executionContext: ExecutionContext = {\n baseUrl,\n headers: {\n cookie: req.headers.get(\"cookie\") ?? \"\",\n authorization: req.headers.get(\"authorization\") ?? \"\",\n },\n };\n\n // Convert tool definitions to AI SDK v6 tool() format\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const aiTools: Record<string, any> = {};\n for (const toolDef of toolDefs) {\n aiTools[toolDef.name] = tool({\n description: toolDef.description,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n inputSchema: toolDef.parameters as any,\n // Disable strict schema validation for OpenAI-compatible providers\n // (e.g., Groq) that reject additionalProperties in tool schemas\n strict: false,\n execute: async (params: Record<string, unknown>) => {\n const result = await executeTool(\n toolDef,\n params,\n executionContext,\n );\n return result.response;\n },\n });\n }\n\n const modelMessages = await convertToModelMessages(messages as UIMessage[]);\n\n // Limit history if configured\n const maxHistory = config.maxHistoryMessages ?? 20;\n const trimmedMessages =\n modelMessages.length > maxHistory\n ? modelMessages.slice(-maxHistory)\n : modelMessages;\n\n // Resolve system prompt — supports static string or async function\n const defaultPrompt = `You are an AI assistant for this application. You can help users query data and perform actions. User: ${session.user.id}${session.user.role ? ` (role: ${session.user.role})` : \"\"}`;\n const systemPromptValue =\n typeof config.systemPrompt === \"function\"\n ? await config.systemPrompt(session)\n : config.systemPrompt ?? defaultPrompt;\n\n const result = streamText({\n model: config.model,\n system: systemPromptValue,\n messages: trimmedMessages,\n tools: aiTools,\n stopWhen: stepCountIs(5),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ...(config.providerOptions ? { providerOptions: config.providerOptions as any } : {}),\n });\n\n return result.toUIMessageStreamResponse();\n}\n"],"mappings":"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","names":["fs","path","path","fs","routes","result"]}
|
package/package.json
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-me-chat/nextjs",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "AI-Me Next.js integration — handler,
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AI-Me Next.js integration — route handler factory, auto-discovery, and proxy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ai",
|
|
8
|
+
"copilot",
|
|
9
|
+
"nextjs",
|
|
10
|
+
"app-router",
|
|
11
|
+
"route-handler",
|
|
12
|
+
"ai-assistant",
|
|
13
|
+
"vercel-ai-sdk",
|
|
14
|
+
"openapi"
|
|
15
|
+
],
|
|
6
16
|
"repository": {
|
|
7
17
|
"type": "git",
|
|
8
18
|
"url": "https://github.com/aselims/ai-me-chat",
|
|
9
19
|
"directory": "packages/nextjs"
|
|
10
20
|
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
11
24
|
"type": "module",
|
|
12
25
|
"main": "./dist/index.cjs",
|
|
13
26
|
"module": "./dist/index.js",
|
|
@@ -20,7 +33,8 @@
|
|
|
20
33
|
}
|
|
21
34
|
},
|
|
22
35
|
"files": [
|
|
23
|
-
"dist"
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
24
38
|
],
|
|
25
39
|
"sideEffects": false,
|
|
26
40
|
"peerDependencies": {
|
|
@@ -30,7 +44,7 @@
|
|
|
30
44
|
},
|
|
31
45
|
"dependencies": {
|
|
32
46
|
"picomatch": "^4.0.3",
|
|
33
|
-
"@ai-me-chat/core": "0.0
|
|
47
|
+
"@ai-me-chat/core": "0.2.0"
|
|
34
48
|
},
|
|
35
49
|
"devDependencies": {
|
|
36
50
|
"@types/node": "^25.5.0",
|