@ai-me-chat/nextjs 0.1.0 → 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 CHANGED
@@ -10,7 +10,14 @@ npm install @ai-me-chat/nextjs
10
10
 
11
11
  ## Quick Start
12
12
 
13
- Create `app/api/ai-me/route.ts`:
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`:
14
21
 
15
22
  ```typescript
16
23
  import { createAIMeHandler } from "@ai-me-chat/nextjs";
@@ -38,19 +45,95 @@ export { handler as GET, handler as POST };
38
45
 
39
46
  ## Features
40
47
 
41
- - **Filesystem discovery** — auto-scans `app/api/` routes at startup
48
+ - **Filesystem discovery** — auto-scans `app/api/` routes at startup, with `src/app` auto-detection
42
49
  - **OpenAPI discovery** — generate tools from an OpenAPI 3.x spec (inline or remote URL)
43
50
  - **Route filtering** — include/exclude patterns with glob support
44
51
  - **Auth forwarding** — forwards cookies and authorization headers to your routes
45
52
  - **Write confirmation** — POST/PUT/PATCH/DELETE require user confirmation by default
53
+ - **Dynamic system prompt** — inject user-specific context via a function
46
54
 
47
55
  ## Endpoints
48
56
 
49
- | Path | Method | Purpose |
50
- |------|--------|---------|
51
- | `/api/ai-me` | POST | Chat endpoint |
52
- | `/api/ai-me/tools` | GET | List discovered tools |
53
- | `/api/ai-me/health` | GET | Health check |
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
+ ```
54
137
 
55
138
  ## Peer Dependencies
56
139
 
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(appDir) {
147
+ async function getToolDefinitions() {
134
148
  if (toolDefinitions) return toolDefinitions;
135
149
  if (toolsPromise) return toolsPromise;
136
- toolsPromise = initTools(appDir);
150
+ toolsPromise = initTools();
137
151
  toolDefinitions = await toolsPromise;
138
152
  toolsPromise = null;
139
153
  return toolDefinitions;
140
154
  }
141
- async function initTools(appDir) {
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(appDir);
199
+ tools = await getToolDefinitions();
184
200
  } catch (error) {
185
- return new Response(
186
- JSON.stringify({ error: error instanceof Error ? error.message : "Tool discovery failed" }),
187
- { status: 500, headers: { "Content-Type": "application/json" } }
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
- const { messages } = await req.json();
212
- const appDir = process.cwd() + "/app";
213
- const toolDefs = await getToolDefinitions(appDir);
214
- const baseUrl = new URL(req.url).origin;
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
- const def = toolDef;
225
- aiTools[def.name] = (0, import_ai.tool)({
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: def.parameters,
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
- def,
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: config.systemPrompt ?? `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})` : ""}`,
279
+ system: systemPromptValue,
248
280
  messages: trimmedMessages,
249
281
  tools: aiTools,
250
282
  stopWhen: (0, import_ai.stepCountIs)(5),
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,SAAoB;AACpB,WAAsB;AAGtB,IAAM,eAAe,CAAC,OAAO,QAAQ,OAAO,SAAS,QAAQ;AAE7D,IAAM,mBAAmB,CAAC,YAAY,YAAY,aAAa,WAAW;AAOnE,SAAS,WAAW,QAAmC;AAC5D,QAAM,SAAc,UAAK,QAAQ,KAAK;AACtC,MAAI,CAAI,cAAW,MAAM,GAAG;AAC1B,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAA4B,CAAC;AACnC,gBAAc,QAAQ,QAAQ,MAAM;AACpC,SAAO,OAAO,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAC3D;AAEA,SAAS,cACP,KACA,QACA,QACM;AACN,QAAM,UAAa,eAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAE3D,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAgB,UAAK,KAAK,MAAM,IAAI;AAC1C,QAAI,MAAM,YAAY,GAAG;AACvB,oBAAc,UAAU,QAAQ,MAAM;AAAA,IACxC,WAAW,iBAAiB,SAAS,MAAM,IAAI,GAAG;AAChD,YAAM,QAAQ,eAAe,UAAU,MAAM;AAC7C,UAAI,SAAS,MAAM,QAAQ,SAAS,GAAG;AACrC,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,eAAe,UAAkB,QAAwC;AAChF,QAAM,UAAa,gBAAa,UAAU,OAAO;AACjD,QAAM,UAAU,mBAAmB,OAAO;AAE1C,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,eAAoB,cAAS,QAAa,aAAQ,QAAQ,CAAC;AACjE,QAAM,UAAU,MAAM,aAAa,MAAW,QAAG,EAAE,KAAK,GAAG;AAC3D,QAAM,aAAa,kBAAkB,OAAO;AAE5C,SAAO;AAAA,IACL,MAAM,UAAU,OAAO;AAAA,IACvB;AAAA,IACA;AAAA,IACA,UAAe,cAAc,aAAQ,QAAQ,IAAI,GAAG,QAAQ;AAAA,EAC9D;AACF;AAUA,SAAS,mBAAmB,SAA2B;AACrD,QAAM,UAAoB,CAAC;AAE3B,aAAW,UAAU,cAAc;AACjC,UAAM,WAAW;AAAA;AAAA,MAEf,IAAI,OAAO,qCAAqC,MAAM,SAAS;AAAA;AAAA,MAE/D,IAAI,OAAO,sBAAsB,MAAM,OAAO;AAAA;AAAA,MAE9C,IAAI,OAAO,8BAA8B,MAAM,aAAa;AAAA,IAC9D;AAEA,QAAI,SAAS,KAAK,CAAC,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG;AACzC,cAAQ,KAAK,MAAM;AAAA,IACrB;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,kBAAkB,WAA6B;AACtD,QAAM,SAAmB,CAAC;AAC1B,QAAM,UAAU,UAAU,SAAS,eAAe;AAClD,aAAW,SAAS,SAAS;AAC3B,WAAO,KAAK,MAAM,CAAC,CAAC;AAAA,EACtB;AACA,SAAO;AACT;AAOA,SAAS,UAAU,WAA2B;AAC5C,SAAO,UACJ,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,oBAAoB,MAAM,EAClC,QAAQ,cAAc,KAAK;AAChC;;;ACnHA,uBAAsB;AASf,SAAS,aACd,QACA,QACmB;AACnB,MAAI,WAAW;AAEf,MAAI,OAAO,WAAW,OAAO,QAAQ,SAAS,GAAG;AAC/C,UAAM,iBAAa,iBAAAA,SAAU,OAAO,OAAO;AAC3C,eAAW,SAAS,OAAO,CAAC,MAAM,WAAW,EAAE,IAAI,CAAC;AAAA,EACtD;AAEA,MAAI,OAAO,WAAW,OAAO,QAAQ,SAAS,GAAG;AAC/C,UAAM,iBAAa,iBAAAA,SAAU,OAAO,OAAO;AAC3C,eAAW,SAAS,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC;AAAA,EACvD;AAEA,SAAO;AACT;;;AC1BA,gBAAe;AACf,kBAAiB;AACjB,gBAAsE;AAEtE,kBAOO;AAaA,SAAS,eAAuB;AACrC,QAAM,SAAS,YAAAC,QAAK,KAAK,QAAQ,IAAI,GAAG,OAAO,KAAK;AACpD,MAAI,UAAAC,QAAG,WAAW,MAAM,EAAG,QAAO;AAClC,SAAO,YAAAD,QAAK,KAAK,QAAQ,IAAI,GAAG,KAAK;AACvC;AAEO,SAAS,cAAc,QAA4B;AACxD,MAAI,OAAO,UAAU,QAAQ;AAC3B,WAAO,YAAAA,QAAK,QAAQ,QAAQ,IAAI,GAAG,OAAO,UAAU,MAAM;AAAA,EAC5D;AACA,SAAO,aAAa;AACtB;AASO,SAAS,kBAAkB,QAAoB;AAGpD,QAAM,SAAS,cAAc,MAAM;AAGnC,MAAI,kBAA+C;AACnD,MAAI,eAAqD;AAEzD,iBAAe,qBAAoD;AACjE,QAAI,gBAAiB,QAAO;AAC5B,QAAI,aAAc,QAAO;AAEzB,mBAAe,UAAU;AACzB,sBAAkB,MAAM;AACxB,mBAAe;AACf,WAAO;AAAA,EACT;AAEA,iBAAe,YAA2C;AACxD,QAAI,OAAO,UAAU,SAAS,WAAW;AACvC,UAAI;AACJ,UAAI,OAAO,UAAU,MAAM;AACzB,eAAO,OAAO,UAAU;AAAA,MAC1B,WAAW,OAAO,UAAU,SAAS;AACnC,eAAO,UAAM,8BAAiB,OAAO,UAAU,OAAO;AAAA,MACxD,OAAO;AACL,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,YAAM,YAAQ,sCAAyB,MAAM,OAAO,YAAY;AAChE,UAAI,OAAO,UAAU,WAAW,OAAO,UAAU,SAAS;AACxD,cAAME,UAAS,MAAM,IAAI,CAAC,OAAO;AAAA,UAC/B,MAAM,EAAE,QAAQ;AAAA,UAChB,SAAS,CAAC,EAAE,cAAc,KAAK;AAAA,UAC/B,YAAY,CAAC;AAAA,UACb,UAAU;AAAA,QACZ,EAAE;AACF,cAAM,WAAW,aAAaA,SAAQ,OAAO,SAAS;AACtD,cAAM,gBAAgB,IAAI;AAAA,UACxB,SAAS,QAAQ,CAAC,MAAM,EAAE,QAAQ,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,QAChE;AACA,eAAO,MAAM,OAAO,CAAC,MAAM,cAAc,IAAI,GAAG,EAAE,UAAU,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,MAC3E;AACA,aAAO;AAAA,IACT;AAEA,QAAI,SAAS,WAAW,MAAM;AAC9B,aAAS,aAAa,QAAQ,OAAO,SAAS;AAC9C,eAAO,qCAAwB,QAAQ,OAAO,YAAY;AAAA,EAC5D;AAEA,iBAAe,QAAQ,KAAiC;AACtD,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAG3B,QAAI,IAAI,WAAW,SAAS,IAAI,SAAS,SAAS,SAAS,GAAG;AAC5D,aAAO,SAAS,KAAK,EAAE,QAAQ,KAAK,CAAC;AAAA,IACvC;AAGA,UAAM,UAAU,MAAM,OAAO,WAAW,GAAG;AAC3C,QAAI,CAAC,SAAS;AACZ,aAAO,IAAI,SAAS,gBAAgB,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrD;AAGA,QAAI,IAAI,WAAW,SAAS,IAAI,SAAS,SAAS,QAAQ,GAAG;AAC3D,UAAI;AACJ,UAAI;AACF,gBAAQ,MAAM,mBAAmB;AAAA,MACnC,SAAS,OAAO;AACd,eAAO,SAAS;AAAA,UACd,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,wBAAwB;AAAA,UAC1E,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AACA,aAAO,SAAS;AAAA,QACd,MAAM,IAAI,CAAC,OAAO;AAAA,UAChB,MAAM,EAAE;AAAA,UACR,aAAa,EAAE;AAAA,UACf,YAAY,EAAE;AAAA,UACd,MAAM,EAAE;AAAA,UACR,sBAAsB,EAAE;AAAA,QAC1B,EAAE;AAAA,MACJ;AAAA,IACF;AAGA,QAAI,IAAI,WAAW,QAAQ;AACzB,aAAO,WAAW,KAAK,QAAQ,SAAS,oBAAoB,IAAI,MAAM;AAAA,IACxE;AAEA,WAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClD;AAEA,SAAO;AACT;AAEA,eAAe,WACb,KACA,QACA,SACA,oBACA,SACmB;AAEnB,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,SAAS,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACtE;AAEA,QAAM,EAAE,SAAS,IAAI;AACrB,MAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,GAAG;AACrD,WAAO,SAAS,KAAK,EAAE,OAAO,qCAAqC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvF;AAGA,aAAW,OAAO,UAAU;AAC1B,QAAI,CAAC,IAAI,MAAM,CAAC,IAAI,MAAM;AACxB,aAAO,SAAS;AAAA,QACd;AAAA,UACE,OAAO;AAAA,UACP,UAAU,OAAO,KAAK,GAAG;AAAA,QAC3B;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,MAAM,mBAAmB;AAC1C,QAAM,mBAAqC;AAAA,IACzC;AAAA,IACA,SAAS;AAAA,MACP,QAAQ,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAAA,MACrC,eAAe,IAAI,QAAQ,IAAI,eAAe,KAAK;AAAA,IACrD;AAAA,EACF;AAIA,QAAM,UAA+B,CAAC;AACtC,aAAW,WAAW,UAAU;AAC9B,YAAQ,QAAQ,IAAI,QAAI,gBAAK;AAAA,MAC3B,aAAa,QAAQ;AAAA;AAAA,MAErB,aAAa,QAAQ;AAAA;AAAA;AAAA,MAGrB,QAAQ;AAAA,MACR,SAAS,OAAO,WAAoC;AAClD,cAAMC,UAAS,UAAM;AAAA,UACnB;AAAA,UACA;AAAA,UACA;AAAA,QACF;AACA,eAAOA,QAAO;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,gBAAgB,UAAM,kCAAuB,QAAuB;AAG1E,QAAM,aAAa,OAAO,sBAAsB;AAChD,QAAM,kBACJ,cAAc,SAAS,aACnB,cAAc,MAAM,CAAC,UAAU,IAC/B;AAGN,QAAM,gBAAgB,0GAA0G,QAAQ,KAAK,EAAE,GAAG,QAAQ,KAAK,OAAO,WAAW,QAAQ,KAAK,IAAI,MAAM,EAAE;AAC1M,QAAM,oBACJ,OAAO,OAAO,iBAAiB,aAC3B,MAAM,OAAO,aAAa,OAAO,IACjC,OAAO,gBAAgB;AAE7B,QAAM,aAAS,sBAAW;AAAA,IACxB,OAAO,OAAO;AAAA,IACd,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,cAAU,uBAAY,CAAC;AAAA;AAAA,IAEvB,GAAI,OAAO,kBAAkB,EAAE,iBAAiB,OAAO,gBAAuB,IAAI,CAAC;AAAA,EACrF,CAAC;AAED,SAAO,OAAO,0BAA0B;AAC1C;","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(appDir) {
114
+ async function getToolDefinitions() {
101
115
  if (toolDefinitions) return toolDefinitions;
102
116
  if (toolsPromise) return toolsPromise;
103
- toolsPromise = initTools(appDir);
117
+ toolsPromise = initTools();
104
118
  toolDefinitions = await toolsPromise;
105
119
  toolsPromise = null;
106
120
  return toolDefinitions;
107
121
  }
108
- async function initTools(appDir) {
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(appDir);
166
+ tools = await getToolDefinitions();
151
167
  } catch (error) {
152
- return new Response(
153
- JSON.stringify({ error: error instanceof Error ? error.message : "Tool discovery failed" }),
154
- { status: 500, headers: { "Content-Type": "application/json" } }
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
- const { messages } = await req.json();
179
- const appDir = process.cwd() + "/app";
180
- const toolDefs = await getToolDefinitions(appDir);
181
- const baseUrl = new URL(req.url).origin;
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
- const def = toolDef;
192
- aiTools[def.name] = tool({
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: def.parameters,
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
- def,
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: config.systemPrompt ?? `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})` : ""}`,
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-me-chat/nextjs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AI-Me Next.js integration — route handler factory, auto-discovery, and proxy",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "picomatch": "^4.0.3",
47
- "@ai-me-chat/core": "0.1.0"
47
+ "@ai-me-chat/core": "0.2.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^25.5.0",