UNPKG

@inkeep/create-agents

Version:

Create an Inkeep Agent Framework project

383 lines (373 loc) 14.4 kB
import * as p from '@clack/prompts'; import { exec } from 'child_process'; import fs from 'fs-extra'; import path from 'path'; import color from 'picocolors'; import { promisify } from 'util'; import { cloneTemplate, getAvailableTemplates } from './templates.js'; const execAsync = promisify(exec); export const defaultGoogleModelConfigurations = { base: { model: 'google/gemini-2.5-flash', }, structuredOutput: { model: 'google/gemini-2.5-flash-lite', }, summarizer: { model: 'google/gemini-2.5-flash-lite', }, }; export const defaultOpenaiModelConfigurations = { base: { model: 'openai/gpt-5-2025-08-07', }, structuredOutput: { model: 'openai/gpt-4.1-mini-2025-04-14', }, summarizer: { model: 'openai/gpt-4.1-nano-2025-04-14', }, }; export const defaultAnthropicModelConfigurations = { base: { model: 'anthropic/claude-sonnet-4-20250514', }, structuredOutput: { model: 'anthropic/claude-3-5-haiku-20241022', }, summarizer: { model: 'anthropic/claude-3-5-haiku-20241022', }, }; export const createAgents = async (args = {}) => { let { dirName, openAiKey, anthropicKey, googleKey, template, customProjectId } = args; const tenantId = 'default'; const manageApiPort = '3002'; const runApiPort = '3003'; let projectId; let templateName; // Determine project ID and template based on user input if (customProjectId) { // User provided custom project ID - use it as-is, no template needed projectId = customProjectId; templateName = ''; // No template will be cloned } else if (template) { // User provided template - validate it exists and use template name as project ID const availableTemplates = await getAvailableTemplates(); if (!availableTemplates.includes(template)) { p.cancel(`${color.red('✗')} Template "${template}" not found\n\n` + `${color.yellow('Available templates:')}\n` + ` • ${availableTemplates.join('\n • ')}\n`); process.exit(0); } projectId = template; templateName = template; } else { // No template or custom project ID provided - use defaults projectId = 'weather-project'; templateName = 'weather-project'; } p.intro(color.inverse(' Create Agents Directory ')); // Prompt for directory name if not provided if (!dirName) { const dirResponse = await p.text({ message: 'What do you want to name your agents directory?', placeholder: 'agents', defaultValue: 'agents', validate: (value) => { if (!value || value.trim() === '') { return 'Directory name is required'; } return undefined; }, }); if (p.isCancel(dirResponse)) { p.cancel('Operation cancelled'); process.exit(0); } dirName = dirResponse; } // Project ID is already determined above based on template/customProjectId logic // If keys aren't provided via CLI args, prompt for provider selection and keys if (!anthropicKey && !openAiKey) { const providerChoice = await p.select({ message: 'Which AI provider would you like to use?', options: [ { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'google', label: 'Google' }, ], }); if (p.isCancel(providerChoice)) { p.cancel('Operation cancelled'); process.exit(0); } // Prompt for keys based on selection if (providerChoice === 'anthropic') { const anthropicKeyResponse = await p.text({ message: 'Enter your Anthropic API key:', placeholder: 'sk-ant-...', validate: (value) => { if (!value || value.trim() === '') { return 'Anthropic API key is required'; } return undefined; }, }); if (p.isCancel(anthropicKeyResponse)) { p.cancel('Operation cancelled'); process.exit(0); } anthropicKey = anthropicKeyResponse; } else if (providerChoice === 'openai') { const openAiKeyResponse = await p.text({ message: 'Enter your OpenAI API key:', placeholder: 'sk-...', validate: (value) => { if (!value || value.trim() === '') { return 'OpenAI API key is required'; } return undefined; }, }); if (p.isCancel(openAiKeyResponse)) { p.cancel('Operation cancelled'); process.exit(0); } openAiKey = openAiKeyResponse; } else if (providerChoice === 'google') { const googleKeyResponse = await p.text({ message: 'Enter your Google API key:', placeholder: 'AIzaSy...', validate: (value) => { if (!value || value.trim() === '') { return 'Google API key is required'; } return undefined; }, }); if (p.isCancel(googleKeyResponse)) { p.cancel('Operation cancelled'); process.exit(0); } googleKey = googleKeyResponse; } } let defaultModelSettings = {}; if (anthropicKey) { defaultModelSettings = defaultAnthropicModelConfigurations; } else if (openAiKey) { defaultModelSettings = defaultOpenaiModelConfigurations; } else if (googleKey) { defaultModelSettings = defaultGoogleModelConfigurations; } const s = p.spinner(); s.start('Creating directory structure...'); try { const agentsTemplateRepo = 'https://github.com/inkeep/create-agents-template'; const projectTemplateRepo = templateName ? `https://github.com/inkeep/agents-cookbook/template-projects/${templateName}` : null; const directoryPath = path.resolve(process.cwd(), dirName); // Check if directory already exists if (await fs.pathExists(directoryPath)) { s.stop(); const overwrite = await p.confirm({ message: `Directory ${dirName} already exists. Do you want to overwrite it?`, }); if (p.isCancel(overwrite) || !overwrite) { p.cancel('Operation cancelled'); process.exit(0); } s.start('Cleaning existing directory...'); await fs.emptyDir(directoryPath); } // Clone the template repository s.message('Building template...'); await cloneTemplate(agentsTemplateRepo, directoryPath); // Change to the project directory process.chdir(directoryPath); const config = { dirName, tenantId, projectId, openAiKey, anthropicKey, googleKey, manageApiPort: manageApiPort || '3002', runApiPort: runApiPort || '3003', modelSettings: defaultModelSettings, customProject: customProjectId ? true : false, }; // Create workspace structure for project-specific files s.message('Setting up project structure...'); await createWorkspaceStructure(); // Create environment files s.message('Setting up environment files...'); await createEnvironmentFiles(config); // Create project template folder (only if template is specified) if (projectTemplateRepo) { s.message('Creating project template folder...'); const templateTargetPath = `src/${projectId}`; await cloneTemplate(projectTemplateRepo, templateTargetPath); } else { s.message('Creating empty project folder...'); await fs.ensureDir(`src/${projectId}`); } // create or overwrite inkeep.config.ts s.message('Creating inkeep.config.ts...'); await createInkeepConfig(config); // Install dependencies s.message('Installing dependencies (this may take a while)...'); await installDependencies(); // Setup database s.message('Setting up database...'); await setupDatabase(); // Setup project in database s.message('Pushing project...'); await setupProjectInDatabase(config); s.message('Project setup complete!'); s.stop(); // Success message with next steps p.note(`${color.green('✓')} Project created at: ${color.cyan(directoryPath)}\n\n` + `${color.yellow('Ready to go!')}\n\n` + `${color.green('✓')} Project created in file system\n` + `${color.green('✓')} Database configured\n` + `${color.green('✓')} Project added to database\n\n` + `${color.yellow('Next steps:')}\n` + ` cd ${dirName}\n` + ` pnpm dev # Start development servers\n\n` + `${color.yellow('Available services:')}\n` + ` • Manage API: http://localhost:${manageApiPort || '3002'}\n` + ` • Run API: http://localhost:${runApiPort || '3003'}\n` + ` • Manage UI: Available with management API\n` + `\n${color.yellow('Configuration:')}\n` + ` • Edit .env for environment variables\n` + ` • Edit files in src/${projectId}/ for agent definitions\n` + ` • Use 'inkeep push' to deploy agents to the platform\n` + ` • Use 'inkeep chat' to test your agents locally\n`, 'Ready to go!'); } catch (error) { s.stop(); p.cancel(`Error creating directory: ${error instanceof Error ? error.message : 'Unknown error'}`); process.exit(1); } }; async function createWorkspaceStructure() { // Create the workspace directory structure await fs.ensureDir(`src`); } async function createEnvironmentFiles(config) { // Root .env file const envContent = `# Environment ENVIRONMENT=development # Database DB_FILE_NAME=file:${process.cwd()}/local.db # AI Provider Keys ANTHROPIC_API_KEY=${config.anthropicKey || 'your-anthropic-key-here'} OPENAI_API_KEY=${config.openAiKey || 'your-openai-key-here'} GOOGLE_GENERATIVE_AI_API_KEY=${config.googleKey || 'your-google-key-here'} # Inkeep API URLs INKEEP_AGENTS_MANAGE_API_URL="http://localhost:3002" INKEEP_AGENTS_RUN_API_URL="http://localhost:3003" # SigNoz Configuration SIGNOZ_URL=your-signoz-url-here SIGNOZ_API_KEY=your-signoz-api-key-here # OTEL Configuration OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ingest.us.signoz.cloud:443/v1/traces OTEL_EXPORTER_OTLP_TRACES_HEADERS="signoz-ingestion-key=<your-ingestion-key>" # Nango Configuration NANGO_SECRET_KEY= `; await fs.writeFile('.env', envContent); } async function createInkeepConfig(config) { const inkeepConfig = `import { defineConfig } from '@inkeep/agents-cli/config'; const config = defineConfig({ tenantId: "${config.tenantId}", projectId: "${config.projectId}", agentsManageApiUrl: 'http://localhost:3002', agentsRunApiUrl: 'http://localhost:3003', modelSettings: ${JSON.stringify(config.modelSettings, null, 2)}, }); export default config;`; await fs.writeFile(`src/${config.projectId}/inkeep.config.ts`, inkeepConfig); if (config.customProject) { const customIndexContent = `import { project } from '@inkeep/agents-sdk'; export const myProject = project({ id: "${config.projectId}", name: "${config.projectId}", description: "", graphs: () => [], });`; await fs.writeFile(`src/${config.projectId}/index.ts`, customIndexContent); } } async function installDependencies() { await execAsync('pnpm install'); } async function setupProjectInDatabase(config) { // Start development servers in background const { spawn } = await import('child_process'); const devProcess = spawn('pnpm', ['dev:apis'], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, // Detach so we can kill the process group cwd: process.cwd(), }); // Give servers time to start await new Promise((resolve) => setTimeout(resolve, 5000)); // Run inkeep push try { // Suppress all output const { stdout, stderr } = await execAsync(`pnpm inkeep push --project src/${config.projectId}`); } catch (error) { //Continue despite error - user can setup project manually } finally { // Kill the dev servers and their child processes if (devProcess.pid) { try { // Kill the entire process group process.kill(-devProcess.pid, 'SIGTERM'); // Wait a moment for graceful shutdown await new Promise((resolve) => setTimeout(resolve, 1000)); // Force kill if still running try { process.kill(-devProcess.pid, 'SIGKILL'); } catch { // Process already terminated } } catch (error) { // Process might already be dead, that's fine console.log('Note: Dev servers may still be running in background'); } } } } async function setupDatabase() { try { // Run drizzle-kit push to create database file and apply schema await execAsync('pnpm db:push'); await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { throw new Error(`Failed to setup database: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Export the command function for the CLI export async function createCommand(dirName, options) { await createAgents({ dirName, ...options, }); }