diff --git a/apps/code/app/api/agent/[[...slug]]/route.ts b/apps/code/app/api/agent/[[...slug]]/route.ts index f23dca2b7..73365a605 100644 --- a/apps/code/app/api/agent/[[...slug]]/route.ts +++ b/apps/code/app/api/agent/[[...slug]]/route.ts @@ -1,32 +1,32 @@ import { AiRouter } from '@microfox/ai-router'; -import { tracingMiddleware } from '@/lib/middleware'; -import { slsfoxAgent } from '@/lib/agents/slsFox'; +import { tracingMiddleware } from '@/lib/middlewares/tracingAgents'; +import { receptionistAgent } from '@/lib/agents'; -// This router will handle direct calls to individual agents. const agentRouter = new AiRouter(); -// Apply the tracing middleware to all agents mounted on this router. agentRouter.use('*', tracingMiddleware); -// Mount the individual agent routers here. -agentRouter.agent('/slsfox', slsfoxAgent); +agentRouter.agent('/', receptionistAgent); // We will export a POST handler that processes all incoming requests. // The `[[...slug]]` in the filename makes this a catch-all route under /agent. export async function POST(request: Request, { params }: { params: { slug: string[] } }) { // Determine the path from the URL slug. const path = `/${(params.slug || []).join('/')}`; + + const body = await request.json(); + const { messages, ...restOfBody } = body; + console.log("body", body) + console.log("messages", messages) + console.log("path", path) - const body = await request.json() - - console.log("body", body, path) // Let the AiRouter handle the request and generate a response. // We add an empty 'messages' array to the request to satisfy the router's type. const response = agentRouter.handle(path, { request: { - ...body, - messages: [] + ...restOfBody, + messages: messages || [] } }); diff --git a/apps/code/app/api/orchestrate/route.ts b/apps/code/app/api/orchestrate/route.ts deleted file mode 100644 index 9baa387a5..000000000 --- a/apps/code/app/api/orchestrate/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { orchestratorAgent } from '@/lib/agents/orchestrator'; -import { tracingMiddleware } from '@/lib/middleware'; - -// Apply the tracing middleware to all routes in the orchestrator agent. -orchestratorAgent.use('*', tracingMiddleware); - -// We will export a POST handler that processes all incoming requests. -export async function POST(request: Request) { - // Let the orchestrator agent handle the request and generate a response. - // We pass '/' as the path because the orchestrator's base path is already set. - // We add an empty 'messages' array to the request to satisfy the router's type. - const body = await request.json() - - console.log("body", body) - - // Let the AiRouter handle the request and generate a response. - // We add an empty 'messages' array to the request to satisfy the router's type. - const response = orchestratorAgent.handle('/', { - request: { - ...body, - messages: [] - } - }); - - // Return the response generated by the agent. - return response; -} \ No newline at end of file diff --git a/apps/code/helpers/utils.ts b/apps/code/helpers/utils.ts deleted file mode 100644 index 80795bb92..000000000 --- a/apps/code/helpers/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as path from 'path'; -import * as fs from 'fs'; - -export function getProjectRoot(): string { - let currentDir = __dirname; - while (!fs.existsSync(path.join(currentDir, 'package.json'))) { - currentDir = path.dirname(currentDir); - } - return path.join(currentDir, '..', '..', '..') -} diff --git a/apps/code/lib/agents/code.ts b/apps/code/lib/agents/code.ts index 5a6d5e5f2..946cae091 100644 --- a/apps/code/lib/agents/code.ts +++ b/apps/code/lib/agents/code.ts @@ -1,44 +1,61 @@ import { AiRouter } from '@microfox/ai-router'; import { generateCodeV2 } from '@microfox/ai-code'; import { google } from '@ai-sdk/google'; +import { anthropic } from '@ai-sdk/anthropic'; import { z } from 'zod'; +import * as path from 'path'; export const codeAgent = new AiRouter(); const codeSchema = z.object({ - // Input comes from the context. + prompt: z.string().describe('The detailed prompt or description for the code to be generated.'), }); codeAgent - .actAsTool('/code', { - description: 'Generates code based on documentation from the context.', + .actAsTool('/', { + description: 'Generates code from a prompt or description.', inputSchema: codeSchema as any, }) - .agent('/code', async ({ state, response }: any) => { - const taskContext = state; - const prompt = taskContext.documentation || taskContext.initialPrompt; + .agent('/', async (ctx) => { + const prompt = ctx.request.params?.prompt as string if (!prompt) { - const errorMsg = 'Error: No prompt for code generation found in the context.'; - response.write({ type: 'text', text: errorMsg }); - taskContext.finalOutput = errorMsg; + const errorMsg = 'Error: No prompt for code generation provided.'; + ctx.response.write({ type: 'text', text: errorMsg }); return; } try { + let isFirstChunk = true; await generateCodeV2({ - model: google('gemini-2.5-pro-preview-06-05'), + model: anthropic('claude-4-opus-20250514'), + submodel: google('gemini-2.5-pro-preview-06-05'), systemPrompt: 'You are an expert programmer. Generate the code for the file as requested.', userPrompt: prompt, + dir: process.cwd(), + verbose: true, + onChunkSubmit: async ({ chunk, filePlan }) => { + if (isFirstChunk) { + const fullFileName = `${filePlan.fileName}.${filePlan.fileExtension}`; + const finalPath = path.join(filePlan.path, fullFileName); + ctx.response.write({ + type: 'data-metadata', + data: { + filePath: finalPath, + fileName: fullFileName, + }, + }); + isFirstChunk = false; + } + ctx.response.write({ type: 'text', text: chunk }); + }, onFileSubmit: async (filePath: string, code: string) => { - taskContext.code = code; - taskContext.finalOutput = code; - response.write({ type: 'text', text: code }); + ctx.response.write({ type: 'text', text: '\n[GENERATION COMPLETE]' }); + console.log(`File generation complete for ${filePath}. Total length: ${code.length}`); }, }); } catch (error: any) { const errorMessage = `Error generating code: ${error.message}`; - taskContext.finalOutput = errorMessage; - response.write({ type: 'text', text: errorMessage }); + ctx.response.write({ type: 'text', text: errorMessage }); } }); \ No newline at end of file diff --git a/apps/code/lib/agents/docs.ts b/apps/code/lib/agents/docs.ts index 58c741e3f..ef9b7ba15 100644 --- a/apps/code/lib/agents/docs.ts +++ b/apps/code/lib/agents/docs.ts @@ -6,30 +6,31 @@ import { z } from 'zod'; export const docsAgent = new AiRouter(); const docsSchema = z.object({ - // Input comes from the context. + topic: z.string().describe('The topic to generate documentation for.'), + context: z.any().describe('The context to generate documentation for.').optional() }); docsAgent - .actAsTool('/docs', { - description: 'Generates documentation based on a summary or topic from the context.', + .actAsTool('/', { + description: 'Generates documentation for a topic. Provide the topic and the context if available.', inputSchema: docsSchema as any, }) - .agent('/docs', async ({ state, response }: any) => { - const taskContext = state; - // This agent can build on the work of the previous one. - const topic = taskContext.summary || taskContext.initialPrompt; + .agent('/', async (ctx) => { + const topic = ctx.request.params?.topic as string + const context = ctx.request.params?.context as any if (!topic) { - response.write({ type: 'text', text: 'Error: No topic for documentation found in the context.' }); + ctx.response.write({ type: 'text', text: 'Error: No topic for documentation provided.' }); return; } const { text: documentation } = await generateText({ model: google('gemini-2.5-pro-preview-06-05'), - prompt: `Please generate technical documentation based on the following: ${topic}`, + prompt: `Please generate technical documentation based on the following: ${topic} + + context: ${JSON.stringify(context)} + `, }); - // Write the result back to the shared context and the response. - taskContext.documentation = documentation; - response.write({ type: 'text', text: documentation }); - }); \ No newline at end of file + ctx.response.write({ type: 'text', text: documentation }); + }); diff --git a/apps/code/lib/agents/index.ts b/apps/code/lib/agents/index.ts new file mode 100644 index 000000000..7efcd1d2a --- /dev/null +++ b/apps/code/lib/agents/index.ts @@ -0,0 +1,50 @@ +import { AiRouter } from '@microfox/ai-router'; +import { generateText } from 'ai'; +import { google } from '@ai-sdk/google'; +import { z } from 'zod'; +import { codeAgent } from '@/lib/agents/code'; +import { docsAgent } from '@/lib/agents/docs'; +import { summarizeAgent } from '@/lib/agents/summarize'; +import { slsfoxAgent } from '@/lib/agents/slsFox'; + +export const receptionistAgent = new AiRouter(); + +const receptionistInput = z.object({ + prompt: z.string().describe('The user a high-level goal to accomplish.'), +}); + +receptionistAgent + .actAsTool('/', { + description: 'Analyzes a user prompt and calls the appropriate agent.', + inputSchema: receptionistInput as any, + }) + .agent('/', async (ctx) => { + const { prompt } = ctx.request + + ctx.response.write({ type: 'text', text: 'Finding the right agent for the job...\n' }); + + const { toolCalls } = await generateText({ + model: google('gemini-2.5-pro-preview-06-05'), + prompt: `You are a highly intelligent routing system. Your purpose is to deeply analyze a user's request to understand its fundamental intent. From this analysis, you will determine the single most appropriate specialized function to execute. + +Analyze the following user prompt: +--- +${prompt} +--- + +Now, determine the core task the user wants to accomplish and invoke the corresponding function.`, + tools: { + slsfox: ctx.next.agentAsTool("/slsfox"), + summarize: ctx.next.agentAsTool("/summarize"), + docs: ctx.next.agentAsTool("/docs"), + code: ctx.next.agentAsTool("/code"), + } + }); + + ctx.response.write({ type: 'text', text: `\nExecution complete. Tool Calls:\n${toolCalls}` }); + }); + +receptionistAgent.agent('/slsfox', slsfoxAgent); +receptionistAgent.agent('/code', codeAgent); +receptionistAgent.agent('/docs', docsAgent); +receptionistAgent.agent('/summarize', summarizeAgent); diff --git a/apps/code/lib/agents/orchestrator.ts b/apps/code/lib/agents/orchestrator.ts deleted file mode 100644 index ffff4e087..000000000 --- a/apps/code/lib/agents/orchestrator.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AiRouter } from '@microfox/ai-router'; -import { generateObject } from 'ai'; -import { google } from '@ai-sdk/google'; -import { z } from 'zod'; - -export const orchestratorAgent = new AiRouter(); - -const orchestratorSchema = z.object({ - // The user's high-level goal. - prompt: z.string().describe('The user a high-level goal to accomplish.'), -}); - -// This is the schema for the execution plan the orchestrator will generate. -const planSchema = z.object({ - plan: z - .array( - z.object({ - agent: z.enum(['summarize', 'docs', 'code']), - details: z.string().describe('The specific goal for this step.'), - }) - ) - .describe('The sequence of agents to call to accomplish the user goal.'), -}); - -orchestratorAgent - .actAsTool('/', { - description: 'Analyzes a user prompt and creates a step-by-step plan of agent calls.', - inputSchema: orchestratorSchema as any, - }) - - .agent('/', async ({ request, response, state, next }: any) => { - const { prompt } = await request.json(); - - // 1. Create the Execution Plan - response.write({ type: 'text', text: 'Generating execution plan...\n' }); - const { object: planResult } = await generateObject({ - model: google('gemini-2.5-pro-preview-06-05'), - prompt: `Based on the following user prompt, create a step-by-step execution plan. The available agents are: 'summarize', 'docs', 'code'. - -User Prompt: ${prompt}`, - schema: planSchema, - }); - - response.write({ type: 'text', text: `Plan: ${JSON.stringify(planResult.plan, null, 2)}\n\n` }); - - // 2. Execute the Plan - // Initialize the shared context that will be passed through the chain. - state.initialPrompt = prompt; - state.executionTrace = []; - - for (const step of planResult.plan) { - response.write({ type: 'text', text: `Executing agent: ${step.agent}...\n` }); - - const result = await next.callAgent(`/agent/${step.agent}`); - - if (!result.ok) { - const errorMsg = `Error executing agent ${step.agent}: ${result.error.message}`; - response.write({ type: 'text', text: errorMsg }); - return; // Stop execution on failure. - } - } - - response.write({ type: 'text', text: `\nExecution complete. Final result:\n${state.finalOutput}` }); - }); \ No newline at end of file diff --git a/apps/code/lib/agents/slsFox/genFullSls.ts b/apps/code/lib/agents/slsFox/genFullSls.ts new file mode 100644 index 000000000..78c2bc4c7 --- /dev/null +++ b/apps/code/lib/agents/slsFox/genFullSls.ts @@ -0,0 +1,42 @@ +import { AiRouter } from '@microfox/ai-router'; +import { z } from 'zod'; +import { generateObject } from 'ai'; +import path from 'path'; +import * as fs from 'fs'; +import { anthropic } from '@ai-sdk/anthropic'; +import { PackageInfo } from '@/lib/types/PackageInfo'; +import { copyDirectory, updateTemplateFiles } from './template'; + +export const genFullSlsAgent = new AiRouter(); + +const fullSlsSchema = z.object({ + packageName: z.string().describe('The name of the package for which to generate the full structure.'), +}); + +genFullSlsAgent + .actAsTool('/', { + description: 'Generates the complete sls structure for a package.', + inputSchema: fullSlsSchema as any + }) + .agent('/', async (ctx) => { + const packageName = ctx.request.params?.packageName as string + ctx.response.write({ type: 'text', text: `Generating full SLS structure for ${packageName}...\n` }); + + try { + const templateDir = path.join(process.cwd(), 'lib', 'agents', 'slsFox', 'template'); + if (fs.existsSync(ctx.state[packageName].slsDir)) { + fs.rmSync(ctx.state[packageName].slsDir, { recursive: true, force: true }); + } + copyDirectory(templateDir, ctx.state[packageName].slsDir); + updateTemplateFiles(ctx.state[packageName].slsDir, packageName, ctx.state[packageName].packageInfo.description); + + await ctx.next.callAgent('@/slsfox/genSdkMap', { packageName }); + await ctx.next.callAgent('@/slsfox/genOpenApi', { packageName }); + await ctx.next.callAgent('@/slsfox/genOpenApiMd', { packageName }); + + ctx.response.write({ type: 'text', text: `Successfully generated serverless structure for ${packageName}.\n` }); + + } catch (error: any) { + ctx.response.write({ type: 'text', text: `Error generating serverless structure: ${error.message}` }); + } + }); diff --git a/apps/code/lib/agents/slsFox/genOpenApi.ts b/apps/code/lib/agents/slsFox/genOpenApi.ts index 58f9ebce3..03dbbdc14 100644 --- a/apps/code/lib/agents/slsFox/genOpenApi.ts +++ b/apps/code/lib/agents/slsFox/genOpenApi.ts @@ -2,9 +2,8 @@ import { AiRouter } from '@microfox/ai-router'; import { z } from 'zod'; import * as fs from 'fs'; import * as path from 'path'; -import { genPathSpecAgent } from './genPathSpec'; -export const genOpenApiAgent = new AiRouter(); +export const genOpenApiAgent = new AiRouter(); const schema = z.object({ packageName: z.string().describe('The name of the package (e.g., "google-sheets").'), @@ -64,7 +63,7 @@ genOpenApiAgent inputSchema: schema as any, }) .agent('/', async (ctx) => { - const { packageName } = ctx.request; + const packageName = ctx.request.params?.packageName as string try { const openapiDir = path.join(ctx.state[packageName].slsDir, 'openapi'); @@ -82,7 +81,10 @@ genOpenApiAgent } console.log('genOpenApiAgent', packageName, funcName); ctx.request.functionName = funcName; - await ctx.next.callAgent('/genPathSpec'); + await ctx.next.callAgent('@/slsfox/genPathSpec', { + packageName, + functionName: funcName, + }); } } @@ -91,6 +93,4 @@ genOpenApiAgent } catch (error: any) { ctx.response.write({ type: 'text', text: `Error generating OpenAPI spec: ${error.message}` }); } - }); - - genOpenApiAgent.agent('/genPathSpec', genPathSpecAgent) + }); diff --git a/apps/code/lib/agents/slsFox/genOpenApiMd.ts b/apps/code/lib/agents/slsFox/genOpenApiMd.ts index a2429ee7a..a4afb26cf 100644 --- a/apps/code/lib/agents/slsFox/genOpenApiMd.ts +++ b/apps/code/lib/agents/slsFox/genOpenApiMd.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { google } from '@ai-sdk/google'; -export const genOpenApiMdAgent = new AiRouter(); +export const genOpenApiMdAgent = new AiRouter(); const schema = z.object({ packageName: z.string().describe('The name of the package (e.g., "google-sheets").'), @@ -65,7 +65,7 @@ async function generateOpenAPIMarkdown( **API Functionality:** ${functionalityMarkdown} `; - + const { object: result } = await generateObject({ model: google('gemini-1.5-pro-latest'), system: systemPrompt, @@ -94,7 +94,7 @@ genOpenApiMdAgent inputSchema: schema as any, }) .agent('/', async (ctx) => { - const { packageName } = ctx.request; + const packageName = ctx.request.params?.packageName as string try { const markdownContent = await generateOpenAPIMarkdown(ctx.state[packageName].packageInfo, ctx.state[packageName].slsDir); diff --git a/apps/code/lib/agents/slsFox/genPathSpec.ts b/apps/code/lib/agents/slsFox/genPathSpec.ts index ab308a109..c4117d3fc 100644 --- a/apps/code/lib/agents/slsFox/genPathSpec.ts +++ b/apps/code/lib/agents/slsFox/genPathSpec.ts @@ -6,7 +6,7 @@ import { anthropic } from '@ai-sdk/anthropic'; import path from 'path'; import * as fs from 'fs'; -export const genPathSpecAgent = new AiRouter(); +export const genPathSpecAgent = new AiRouter(); const schema = z.object({ functionName: z @@ -187,7 +187,8 @@ genPathSpecAgent inputSchema: schema as any, }) .agent('/', async (ctx) => { - const { packageName, functionName } = ctx.request; + const packageName = ctx.request.params?.packageName as string + const functionName = ctx.request.params?.functionName as string console.log('genPathSpecAgent', packageName, functionName); try { const pathSpec = await generateOpenAPIPath( diff --git a/apps/code/lib/agents/slsFox/genSdkMap.ts b/apps/code/lib/agents/slsFox/genSdkMap.ts index e09be10c0..040d62106 100644 --- a/apps/code/lib/agents/slsFox/genSdkMap.ts +++ b/apps/code/lib/agents/slsFox/genSdkMap.ts @@ -4,9 +4,9 @@ import { generateObject } from 'ai'; import path from 'path'; import * as fs from 'fs'; import { anthropic } from '@ai-sdk/anthropic'; -import { PackageInfo } from '@/types/PackageInfo'; +import { PackageInfo } from '@/lib/types/PackageInfo'; -export const genSdkMapAgent = new AiRouter(); +export const genSdkMapAgent = new AiRouter(); const schema = z.object({ packageName: z.string().describe('The name of the package (e.g., "google-sheets").'), @@ -132,7 +132,7 @@ genSdkMapAgent inputSchema: schema as any, }) .agent('/', async (ctx) => { - const { packageName } = ctx.request + const packageName = ctx.request.params?.packageName as string try { const sdkInitContent = await generateSDKInitContent(ctx.state[packageName].packageInfo); diff --git a/apps/code/lib/agents/slsFox/index.ts b/apps/code/lib/agents/slsFox/index.ts index 32ea94f47..e95672488 100644 --- a/apps/code/lib/agents/slsFox/index.ts +++ b/apps/code/lib/agents/slsFox/index.ts @@ -1,82 +1,64 @@ import { AiRouter } from '@microfox/ai-router'; import { z } from 'zod'; -import * as fs from 'fs'; -import * as path from 'path'; -import { getPackageInfo } from '../middlewares/getPackageInfo'; -import { getPackageDocs } from '../middlewares/getPackageDocs'; -import { copyDirectory, updateTemplateFiles } from './utils'; +import { generateText } from 'ai'; +import { google } from '@ai-sdk/google'; +import { getPackageInfo } from '../../middlewares/getPackageInfo'; +import { getPackageDocs } from '../../middlewares/getPackageDocs'; +import { genFullSlsAgent } from './genFullSls'; import { genOpenApiAgent } from './genOpenApi'; import { genOpenApiMdAgent } from './genOpenApiMd'; import { genSdkMapAgent } from './genSdkMap'; +import { genPathSpecAgent } from './genPathSpec'; -export const slsfoxAgent = new AiRouter(); +export const slsfoxAgent = new AiRouter(); const schema = z.object({ - packageName: z.string().describe('The name of the package (e.g., "google-sheets").'), - specificFunctions: z.array(z.string()).optional().describe('Optional list of specific functions to update.'), + packageName: z.string().optional().describe('The name of the package (e.g., "google-sheets").'), + query: z.string().optional().describe('A natural language prompt for what to build or update.'), }); - slsfoxAgent .use('/', getPackageInfo) .use('/', getPackageDocs) .actAsTool('/', { - description: 'Generates the complete serverless structure for a package.', + description: 'Generates or updates the serverless structure for a package based on a prompt.', inputSchema: schema as any, }) .agent('/', async (ctx) => { - const { packageName, specificFunctions } = ctx.request - - console.log("packageName", packageName) - console.log("specificFunctions", specificFunctions) + const { query, packageName: initialPackageName } = ctx.request.params as z.infer; - if (!packageName) { - ctx.response.write({ type: 'text', text: `packageName is required` }); + if (!query && !initialPackageName) { + ctx.response.write({ type: 'text', text: `Either a query or a package name is required.` }); return; } - if (specificFunctions && specificFunctions.length > 0) { - for (const func of specificFunctions) { - ctx.request.functionName = func; - await ctx.next.callAgent('/genOpenApi/genPathSpec'); - } - ctx.response.write({ type: 'text', text: `Successfully generated openapi.json for ${packageName}'s functions ${specificFunctions.join(', ')}.` }); + if (!query){ + await ctx.next.callAgent('/genFullSls', { packageName: initialPackageName }); return; } - try { - // Copy template directory - const templateDir = path.join(process.cwd(), 'lib', 'agents', 'slsFox', 'template'); - if (fs.existsSync(ctx.state[packageName].slsDir)) { - console.log(`⚠️ SLS directory already exists, removing it first...`); - fs.rmSync(ctx.state[packageName].slsDir, { recursive: true, force: true }); - } - copyDirectory(templateDir, ctx.state[packageName].slsDir); - console.log(`✅ Template copied successfully`); - - // Update template files - updateTemplateFiles(ctx.state[packageName].slsDir, packageName, ctx.state[packageName].packageInfo.description); - - // Generate sdkInit.ts - await ctx.next.callAgent('/genSdkMap', { packageName }); + ctx.response.write({ type: 'text', text: 'Deciding which tool to use...\n' }); - // Generate openapi.json - await ctx.next.callAgent('/genOpenApi', { - packageName, - }); + const { toolCalls } = await generateText({ + model: google('gemini-2.5-pro-preview-06-05'), + prompt: `You are an expert system for managing serverless package structures. Based on the user's request, decide which tool to call. Extract the packageName and any other relevant parameters from the request. - // Generate openapi.md - await ctx.next.callAgent('/genOpenApiMd', { - packageName, - }); +User Request: ${query || `Generate full structure for ${initialPackageName}`}`, + tools: { + generateFullSls: ctx.next.agentAsTool('/genFullSls'), + generateOpenApi: ctx.next.agentAsTool('/genOpenApi'), + generateOpenApiMd: ctx.next.agentAsTool('/genOpenApiMd'), + generateSdkMap: ctx.next.agentAsTool('/genSdkMap'), + generatePathSpec: ctx.next.agentAsTool('/genPathSpec'), + } + }); - ctx.response.write({ type: 'text', text: `Successfully generated serverless structure for ${packageName}.` }); + ctx.response.write({ type: 'text', text: `\nExecution complete. Tool Calls:\n${JSON.stringify(toolCalls)}` }); + }); - } catch (error: any) { - ctx.response.write({ type: 'text', text: `Error generating serverless structure: ${error.message}` }); - } - }); - slsfoxAgent.agent('/genOpenApi', genOpenApiAgent) - slsfoxAgent.agent('/genOpenApiMd', genOpenApiMdAgent) - slsfoxAgent.agent('/genSdkMap', genSdkMapAgent) \ No newline at end of file +slsfoxAgent.agent('/genFullSls', genFullSlsAgent) +slsfoxAgent.agent('/genOpenApi', genOpenApiAgent) +slsfoxAgent.agent('/genOpenApiMd', genOpenApiMdAgent) +slsfoxAgent.agent('/genSdkMap', genSdkMapAgent) +slsfoxAgent.agent('/genPathSpec', genPathSpecAgent) diff --git a/apps/code/lib/agents/slsFox/mergePathSpec.ts b/apps/code/lib/agents/slsFox/mergePathSpec.ts index d23d123fe..cdf986d3d 100644 --- a/apps/code/lib/agents/slsFox/mergePathSpec.ts +++ b/apps/code/lib/agents/slsFox/mergePathSpec.ts @@ -15,7 +15,7 @@ genPathSpecAgent inputSchema: schema as any, }) .agent('/', async (ctx) => { - const { packageName } = ctx.request; + const packageName = ctx.request.params?.packageName as string const slsOpenapiPath = path.join(ctx.state[packageName].slsDir, 'openapi.json'); let slsOpenapi = JSON.parse(fs.readFileSync(slsOpenapiPath, 'utf8')); diff --git a/apps/code/lib/agents/slsFox/template.ts b/apps/code/lib/agents/slsFox/template.ts new file mode 100644 index 000000000..b6ac6f327 --- /dev/null +++ b/apps/code/lib/agents/slsFox/template.ts @@ -0,0 +1,75 @@ +import * as fs from 'fs'; +import * as path from 'path'; + + +export function copyDirectory(src: string, dest: string): void { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + + if (entry.isDirectory()) { + const destPath = path.join(dest, entry.name); + copyDirectory(srcPath, destPath); + } else { + let destFileName = entry.name; + if (entry.name.endsWith('.txt')) { + const baseName = entry.name.replace('.txt', ''); + const extensionMap: { [key: string]: string } = { + package: 'package.json', + tsconfig: 'tsconfig.json', + eslint: 'eslint.config.js', + serverless: 'serverless.yml', + openapi: 'openapi.json', + sdkInit: 'sdkInit.ts', + index: 'index.ts', + }; + destFileName = extensionMap[baseName] || `${baseName}.txt`; + } + + const destPath = path.join(dest, destFileName); + fs.copyFileSync(srcPath, destPath); + } + } +} + +export function updateTemplateFiles( + slsDir: string, + packageName: string, + description: string, +): void { + // Update package.json + const packageJsonPath = path.join(slsDir, 'package.json'); + const parentPackageJsonPath = path.join(slsDir, '..', 'package.json'); + const parentPackageJson = JSON.parse( + fs.readFileSync(parentPackageJsonPath, 'utf8'), + ); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + packageJson.name = `public-${packageName}-api`; + packageJson.description = description; + packageJson.dependencies['@microfox/tool-core'] = `^1.0.1`; + packageJson.dependencies[parentPackageJson.name] = + `^${parentPackageJson.version}`; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + console.log(`✅ Updated package.json for ${packageJson.name}`); + } + + // Update serverless.yml + const serverlessPath = path.join(slsDir, 'serverless.yml'); + if (fs.existsSync(serverlessPath)) { + let serverlessContent = fs.readFileSync(serverlessPath, 'utf8'); + serverlessContent = serverlessContent.replace( + /^service:\s+.+$/m, + `service: public-${packageName}-api`, + ); + fs.writeFileSync(serverlessPath, serverlessContent); + console.log( + `✅ Updated serverless.yml for public-${packageName}-api`, + ); + } +} \ No newline at end of file diff --git a/apps/code/lib/agents/slsFox/template/package.txt b/apps/code/lib/agents/slsFox/template/package.txt index 0530c3039..4de92f717 100644 --- a/apps/code/lib/agents/slsFox/template/package.txt +++ b/apps/code/lib/agents/slsFox/template/package.txt @@ -18,9 +18,11 @@ "dependencies": { "@microfox/crypto-sdk": "^1.0.3", "@microfox/tool-core": "^1.0.4", + "aws-lambda": "^1.0.7", "dotenv": "^16.4.5" }, "devDependencies": { + "@types/aws-lambda": "^8.10.150", "@types/node": "^20.11.19", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", diff --git a/apps/code/lib/agents/slsFox/utils.ts b/apps/code/lib/agents/slsFox/utils.ts deleted file mode 100644 index c2b837b78..000000000 --- a/apps/code/lib/agents/slsFox/utils.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import * as fs from 'fs'; -import * as path from 'path'; - - -export function copyDirectory(src: string, dest: string): void { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest, { recursive: true }); - } - - const entries = fs.readdirSync(src, { withFileTypes: true }); - - for (const entry of entries) { - const srcPath = path.join(src, entry.name); - - if (entry.isDirectory()) { - const destPath = path.join(dest, entry.name); - copyDirectory(srcPath, destPath); - } else { - let destFileName = entry.name; - if (entry.name.endsWith('.txt')) { - const baseName = entry.name.replace('.txt', ''); - const extensionMap: { [key: string]: string } = { - package: 'package.json', - tsconfig: 'tsconfig.json', - eslint: 'eslint.config.js', - serverless: 'serverless.yml', - openapi: 'openapi.json', - sdkInit: 'sdkInit.ts', - index: 'index.ts', - }; - destFileName = extensionMap[baseName] || `${baseName}.txt`; - } - - const destPath = path.join(dest, destFileName); - fs.copyFileSync(srcPath, destPath); - } - } - } - - export function updateTemplateFiles( - slsDir: string, - packageName: string, - description: string, - ): void { - // Update package.json - const packageJsonPath = path.join(slsDir, 'package.json'); - const parentPackageJsonPath = path.join(slsDir, '..', 'package.json'); - const parentPackageJson = JSON.parse( - fs.readFileSync(parentPackageJsonPath, 'utf8'), - ); - if (fs.existsSync(packageJsonPath)) { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - packageJson.name = `public-${packageName}-api`; - packageJson.description = description; - packageJson.dependencies['@microfox/tool-core'] = `^1.0.1`; - packageJson.dependencies[parentPackageJson.name] = - `^${parentPackageJson.version}`; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - console.log(`✅ Updated package.json for ${packageJson.name}`); - } - - // Update serverless.yml - const serverlessPath = path.join(slsDir, 'serverless.yml'); - if (fs.existsSync(serverlessPath)) { - let serverlessContent = fs.readFileSync(serverlessPath, 'utf8'); - serverlessContent = serverlessContent.replace( - /^service:\s+.+$/m, - `service: public-${packageName}-api`, - ); - fs.writeFileSync(serverlessPath, serverlessContent); - console.log( - `✅ Updated serverless.yml for public-${packageName}-api`, - ); - } - } \ No newline at end of file diff --git a/apps/code/lib/agents/summarize.ts b/apps/code/lib/agents/summarize.ts index d068d457e..e5fdf920b 100644 --- a/apps/code/lib/agents/summarize.ts +++ b/apps/code/lib/agents/summarize.ts @@ -6,20 +6,19 @@ import { z } from 'zod'; export const summarizeAgent = new AiRouter(); const summarizeSchema = z.object({ - // The input for this agent now comes from the context, so the schema is empty. + textToSummarize: z.string().describe('The text that needs to be summarized.'), }); summarizeAgent - .actAsTool('/summarize', { - description: 'Summarizes the initial prompt or other content found in the task context.', + .actAsTool('/', { + description: 'Summarizes a piece of text.', inputSchema: summarizeSchema as any, }) - .agent('/summarize', async ({ state, response }: any) => { - const taskContext = state; - const textToSummarize = taskContext.initialPrompt; + .agent('/', async (ctx) => { + const textToSummarize = ctx.request.params?.textToSummarize as string if (!textToSummarize) { - response.write({ type: 'text', text: 'Error: No text to summarize found in the context.' }); + ctx.response.write({ type: 'text', text: 'Error: No text to summarize provided.' }); return; } @@ -28,7 +27,6 @@ summarizeAgent prompt: `Please provide a concise summary of the following text: ${textToSummarize}`, }); - // Write the final summary back to the shared context and the response. - taskContext.summary = summary; - response.write({ type: 'text', text: summary }); - }); \ No newline at end of file + ctx.response.write({ type: 'text', text: summary }); + }); + \ No newline at end of file diff --git a/apps/code/lib/helpers/utils.ts b/apps/code/lib/helpers/utils.ts new file mode 100644 index 000000000..9a0609d71 --- /dev/null +++ b/apps/code/lib/helpers/utils.ts @@ -0,0 +1,20 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +export async function getProjectRoot(startPath: string = process.cwd()): Promise { + let currentPath = startPath; + let count = 0 + while (true) { + const microfoxRootPath = path.join(currentPath, 'microfox-root'); + if (fs.existsSync(microfoxRootPath)) { + return currentPath; + } + + const parentPath = path.dirname(currentPath); + if (parentPath === currentPath || count > 10) { + // Reached the root of the file system + return null; + } + currentPath = parentPath; + } +} \ No newline at end of file diff --git a/apps/code/lib/agents/middlewares/getPackageDocs.ts b/apps/code/lib/middlewares/getPackageDocs.ts similarity index 88% rename from apps/code/lib/agents/middlewares/getPackageDocs.ts rename to apps/code/lib/middlewares/getPackageDocs.ts index 53e73be14..cbbfd4227 100644 --- a/apps/code/lib/agents/middlewares/getPackageDocs.ts +++ b/apps/code/lib/middlewares/getPackageDocs.ts @@ -1,11 +1,13 @@ -import { AiMiddleware, AiRouter } from '@microfox/ai-router'; -import { z } from 'zod'; +import { AiMiddleware } from '@microfox/ai-router'; import * as fs from 'fs'; import * as path from 'path'; -import { getProjectRoot } from '@/helpers/utils'; +import { getProjectRoot } from '@/lib/helpers/utils'; export const getPackageDocs: AiMiddleware = async (ctx, next) => { - let { packageName } = ctx.request; + let { packageName } = ctx.request.params; + if (!packageName) { + return next(); + } const taskContext = ctx.state; packageName = packageName.replace('@microfox/', ''); if (taskContext[packageName] && taskContext[packageName].docs) { @@ -13,7 +15,7 @@ export const getPackageDocs: AiMiddleware = async (ctx, next) => { } try { // 1. Initialize Paths - taskContext.projectRoot = getProjectRoot(); + taskContext.projectRoot = await getProjectRoot(); taskContext[packageName] = { ...taskContext[packageName], packageDir: path.join(taskContext.projectRoot, 'packages', packageName), diff --git a/apps/code/lib/agents/middlewares/getPackageInfo.ts b/apps/code/lib/middlewares/getPackageInfo.ts similarity index 87% rename from apps/code/lib/agents/middlewares/getPackageInfo.ts rename to apps/code/lib/middlewares/getPackageInfo.ts index 171d7653e..d9d672bbb 100644 --- a/apps/code/lib/agents/middlewares/getPackageInfo.ts +++ b/apps/code/lib/middlewares/getPackageInfo.ts @@ -1,10 +1,14 @@ import { AiMiddleware } from '@microfox/ai-router'; import * as fs from 'fs'; import * as path from 'path'; -import { getProjectRoot } from '@/helpers/utils'; +import { getProjectRoot } from '@/lib/helpers/utils'; export const getPackageInfo: AiMiddleware = async (ctx, next) => { - let { packageName } = ctx.request + let { packageName } = ctx.request.params + if (!packageName) { + return next(); + } + const taskContext = ctx.state; packageName = packageName.replace('@microfox/', ''); if (taskContext[packageName] && taskContext[packageName].packageInfo) { @@ -13,7 +17,7 @@ export const getPackageInfo: AiMiddleware = async (ctx, next) => { try { // 1. Initialize Paths - taskContext.projectRoot = getProjectRoot(); + taskContext.projectRoot = await getProjectRoot(); taskContext[packageName] = { ...taskContext[packageName], packageDir: path.join(taskContext.projectRoot, 'packages', packageName), diff --git a/apps/code/lib/middleware.ts b/apps/code/lib/middlewares/tracingAgents.ts similarity index 99% rename from apps/code/lib/middleware.ts rename to apps/code/lib/middlewares/tracingAgents.ts index d69efe725..cad0e79df 100644 --- a/apps/code/lib/middleware.ts +++ b/apps/code/lib/middlewares/tracingAgents.ts @@ -13,9 +13,9 @@ export const tracingMiddleware = async (ctx: any, next: () => Promise) => // Get the path of the agent being called from the internal execution context. const path = ctx.executionContext?.currentPath || 'unknown_path'; - + console.log(`[Trace] ==> Entering: ${path}`); - + // Add the current path to our trace log on the state object. if (ctx.state.executionTrace) { ctx.state.executionTrace.push(path); diff --git a/apps/code/types/PackageInfo.d.ts b/apps/code/lib/types/PackageInfo.d.ts similarity index 100% rename from apps/code/types/PackageInfo.d.ts rename to apps/code/lib/types/PackageInfo.d.ts diff --git a/microfox-root b/microfox-root new file mode 100644 index 000000000..e69de29bb diff --git a/packages/cli/package.json b/packages/cli/package.json index 189188095..5463508de 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,11 +30,11 @@ "@types/micromatch": "^4.0.9", "axios": "^1.10.0", "chalk": "^5.4.1", - "commander": "^13.1.0", + "commander": "^14.0.0", "inquirer": "^12.7.0", "micromatch": "^4.0.8", "readline-sync": "^1.4.10", - "zod": "^3.25.67" + "zod": "^4.0.5" }, "devDependencies": { "@microfox/tsconfig": "*", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 8f5d9b900..c3d042657 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -3,6 +3,7 @@ import chalk from 'chalk'; import { kickstartCommand } from './commands/kickstart'; import { pushCommand } from './commands/push'; import { statusCommand, logsCommand, metricsCommand } from './commands/status'; +import { codeCommand } from './commands/code'; import { version } from '../package.json'; const program = new Command(); @@ -73,6 +74,18 @@ program } }); +program + .command('code') + .description('Run the code agent for your project') + .action(async () => { + try { + await codeCommand(); + } catch (error) { + console.error(chalk.red('❌ Error:'), error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); + // Show help if no command is provided if (process.argv.length <= 2) { program.help(); diff --git a/packages/cli/src/commands/code.ts b/packages/cli/src/commands/code.ts new file mode 100644 index 000000000..0e5dcbcf1 --- /dev/null +++ b/packages/cli/src/commands/code.ts @@ -0,0 +1,143 @@ +import chalk from 'chalk'; +import axios from 'axios'; +import { spawn, ChildProcess } from 'child_process'; +import { findProjectRoot } from '../utils/findProjectRoot'; +import path from 'path'; +import readline from 'readline'; + +const NEXTJS_PORT = 3000; +const API_URL = `http://localhost:${NEXTJS_PORT}/api/agent`; + +const createLogger = (rl: readline.Interface) => { + return (source: string, message: string, color: chalk.Chalk) => { + readline.cursorTo(process.stdout, 0); + readline.clearLine(process.stdout, 0); + + const prefix = color(`[${source}]`); + + const lines = message.trim().split('\n'); + for (const line of lines) { + console.log(`${prefix} ${line}`); + } + + rl.prompt(true); + }; +}; + + +export async function codeCommand(): Promise { + let childProcess: ChildProcess | null = null; + + const killProcess = () => { + if (childProcess && childProcess.pid) { + console.log(chalk.yellow('\nGracefully shutting down...')); + if (process.platform === 'win32') { + spawn('taskkill', ['/pid', childProcess.pid.toString(), '/f', '/t']); + } else { + childProcess.kill('SIGINT'); + } + childProcess = null; + } + }; + + process.on('SIGINT', () => { + killProcess(); + process.exit(0); + }); + process.on('exit', killProcess); + + + try { + const projectRoot = await findProjectRoot(); + if (!projectRoot) { + console.error( + chalk.red('Error: Could not find project root. Make sure you are inside a Microfox project.') + ); + process.exit(1); + } + + const codeAppPath = path.join(projectRoot, 'apps', 'code'); + + console.log(chalk.cyan(`Starting Next.js server in ${codeAppPath}...`)); + + childProcess = spawn('npm', ['run', 'dev'], { + cwd: codeAppPath, + shell: true, + env: { ...process.env, FORCE_COLOR: 'true' } + }); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: chalk.cyan('> ') + }); + + const log = createLogger(rl); + + let serverReady = false; + + const onServerData = (data: Buffer) => { + const output = data.toString(); + if (!serverReady) { + process.stdout.write(output); + if (output.toLowerCase().includes('ready in') || output.toLowerCase().includes('compiled successfully')) { + serverReady = true; + console.log(chalk.green('\nServer is ready. You can now type your queries.')); + rl.prompt(); + } + } else { + log('nextjs', output, chalk.gray); + } + }; + + childProcess.stdout?.on('data', onServerData); + childProcess.stderr?.on('data', onServerData); + + childProcess.on('exit', (code) => { + log('system', `Next.js process exited with code ${code}`, chalk.red); + process.exit(code ?? 1); + }); + + rl.on('line', async (line) => { + const query = line.trim(); + if (!serverReady) { + log('system', 'Server is not ready yet, please wait.', chalk.yellow); + rl.prompt(); + return; + } + if (query.toLowerCase() === 'exit') { + rl.close(); + } + if (query) { + try { + const response = await axios.post(API_URL, { prompt: query }); + const responseData = typeof response.data === 'object' + ? JSON.stringify(response.data, null, 2) + : response.data; + + log('agent', responseData, chalk.green); + + } catch (error) { + if (axios.isAxiosError(error)) { + log('agent', `Error: ${error.message}`, chalk.red); + } else if (error instanceof Error) { + log('agent', `An unknown error occurred: ${error.message}`, chalk.red); + } + } + } + rl.prompt(); + }); + + rl.on('close', () => { + killProcess(); + process.exit(0); + }); + + } catch (error) { + killProcess(); + if (error instanceof Error) { + console.error(chalk.red(`Error: ${error.message}`)); + } + process.exit(1); + } +} \ No newline at end of file diff --git a/packages/cli/src/utils/findProjectRoot.ts b/packages/cli/src/utils/findProjectRoot.ts new file mode 100644 index 000000000..e17fc3a1b --- /dev/null +++ b/packages/cli/src/utils/findProjectRoot.ts @@ -0,0 +1,20 @@ +import path from 'path'; +import fs from 'fs'; + +export async function findProjectRoot(startPath: string = process.cwd()): Promise { + let currentPath = startPath; + let count = 0 + while (true) { + const microfoxRootPath = path.join(currentPath, 'microfox-root'); + if (fs.existsSync(microfoxRootPath)) { + return currentPath; + } + + const parentPath = path.dirname(currentPath); + if (parentPath === currentPath || count > 10) { + // Reached the root of the file system + return null; + } + currentPath = parentPath; + } +} \ No newline at end of file