@ai-me-chat/nextjs 0.2.0 → 0.2.1
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/dist/index.cjs +24 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -283,7 +283,30 @@ async function handleChat(req, config, session, getToolDefinitions, baseUrl) {
|
|
|
283
283
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
284
284
|
...config.providerOptions ? { providerOptions: config.providerOptions } : {}
|
|
285
285
|
});
|
|
286
|
-
|
|
286
|
+
const response = result.toUIMessageStreamResponse();
|
|
287
|
+
if (!response.body) return response;
|
|
288
|
+
const transform = new TransformStream({
|
|
289
|
+
transform(chunk, controller) {
|
|
290
|
+
const text = new TextDecoder().decode(chunk);
|
|
291
|
+
const lines = text.split("\n");
|
|
292
|
+
const filtered = lines.filter((line) => {
|
|
293
|
+
if (!line.startsWith("data: ")) return true;
|
|
294
|
+
try {
|
|
295
|
+
const data = JSON.parse(line.slice(6));
|
|
296
|
+
return data.type !== "reasoning-start" && data.type !== "reasoning-end";
|
|
297
|
+
} catch {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
if (filtered.length > 0) {
|
|
302
|
+
controller.enqueue(new TextEncoder().encode(filtered.join("\n")));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
return new Response(response.body.pipeThrough(transform), {
|
|
307
|
+
headers: response.headers,
|
|
308
|
+
status: response.status
|
|
309
|
+
});
|
|
287
310
|
}
|
|
288
311
|
// Annotate the CommonJS export names for ESM import in node:
|
|
289
312
|
0 && (module.exports = {
|
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 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"]}
|
|
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 const response = result.toUIMessageStreamResponse();\n\n // Strip reasoning events (reasoning-start, reasoning-end) from the SSE stream.\n // Some providers (e.g. Groq) emit these even when not requested, and they cause\n // the AI SDK client to create empty message parts that break rendering.\n if (!response.body) return response;\n const transform = new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n const text = new TextDecoder().decode(chunk);\n const lines = text.split(\"\\n\");\n const filtered = lines.filter((line) => {\n if (!line.startsWith(\"data: \")) return true;\n try {\n const data = JSON.parse(line.slice(6));\n return data.type !== \"reasoning-start\" && data.type !== \"reasoning-end\";\n } catch {\n return true;\n }\n });\n if (filtered.length > 0) {\n controller.enqueue(new TextEncoder().encode(filtered.join(\"\\n\")));\n }\n },\n });\n\n return new Response(response.body.pipeThrough(transform), {\n headers: response.headers,\n status: response.status,\n });\n}\n"],"mappings":"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","names":["picomatch","path","fs","routes","result"]}
|
package/dist/index.js
CHANGED
|
@@ -250,7 +250,30 @@ async function handleChat(req, config, session, getToolDefinitions, baseUrl) {
|
|
|
250
250
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
251
251
|
...config.providerOptions ? { providerOptions: config.providerOptions } : {}
|
|
252
252
|
});
|
|
253
|
-
|
|
253
|
+
const response = result.toUIMessageStreamResponse();
|
|
254
|
+
if (!response.body) return response;
|
|
255
|
+
const transform = new TransformStream({
|
|
256
|
+
transform(chunk, controller) {
|
|
257
|
+
const text = new TextDecoder().decode(chunk);
|
|
258
|
+
const lines = text.split("\n");
|
|
259
|
+
const filtered = lines.filter((line) => {
|
|
260
|
+
if (!line.startsWith("data: ")) return true;
|
|
261
|
+
try {
|
|
262
|
+
const data = JSON.parse(line.slice(6));
|
|
263
|
+
return data.type !== "reasoning-start" && data.type !== "reasoning-end";
|
|
264
|
+
} catch {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
if (filtered.length > 0) {
|
|
269
|
+
controller.enqueue(new TextEncoder().encode(filtered.join("\n")));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return new Response(response.body.pipeThrough(transform), {
|
|
274
|
+
headers: response.headers,
|
|
275
|
+
status: response.status
|
|
276
|
+
});
|
|
254
277
|
}
|
|
255
278
|
export {
|
|
256
279
|
createAIMeHandler,
|
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 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"]}
|
|
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 const response = result.toUIMessageStreamResponse();\n\n // Strip reasoning events (reasoning-start, reasoning-end) from the SSE stream.\n // Some providers (e.g. Groq) emit these even when not requested, and they cause\n // the AI SDK client to create empty message parts that break rendering.\n if (!response.body) return response;\n const transform = new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n const text = new TextDecoder().decode(chunk);\n const lines = text.split(\"\\n\");\n const filtered = lines.filter((line) => {\n if (!line.startsWith(\"data: \")) return true;\n try {\n const data = JSON.parse(line.slice(6));\n return data.type !== \"reasoning-start\" && data.type !== \"reasoning-end\";\n } catch {\n return true;\n }\n });\n if (filtered.length > 0) {\n controller.enqueue(new TextEncoder().encode(filtered.join(\"\\n\")));\n }\n },\n });\n\n return new Response(response.body.pipeThrough(transform), {\n headers: response.headers,\n status: response.status,\n });\n}\n"],"mappings":"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","names":["fs","path","path","fs","routes","result"]}
|