@codebakers/cli 3.3.2 → 3.3.4

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/src/mcp/server.ts CHANGED
@@ -1047,6 +1047,170 @@ class CodeBakersServer {
1047
1047
  },
1048
1048
  },
1049
1049
  },
1050
+ {
1051
+ name: 'detect_intent',
1052
+ description:
1053
+ 'CALL THIS FIRST when user request is ambiguous or could match multiple MCP tools. Analyzes user input and returns which MCP tool(s) would be appropriate, with explanations. Show the result to the user and ask for confirmation before executing. This prevents accidental destructive actions.',
1054
+ inputSchema: {
1055
+ type: 'object' as const,
1056
+ properties: {
1057
+ userMessage: {
1058
+ type: 'string',
1059
+ description: 'The user\'s message or request to analyze',
1060
+ },
1061
+ },
1062
+ required: ['userMessage'],
1063
+ },
1064
+ },
1065
+ // VAPI Voice AI Tools
1066
+ {
1067
+ name: 'vapi_connect',
1068
+ description:
1069
+ 'Connect to VAPI (Voice AI Platform). Sets up VAPI API credentials for voice assistant integration. Use when user says "connect vapi", "setup voice", "configure vapi", or before using other vapi_* tools.',
1070
+ inputSchema: {
1071
+ type: 'object' as const,
1072
+ properties: {
1073
+ apiKey: {
1074
+ type: 'string',
1075
+ description: 'VAPI API key from https://dashboard.vapi.ai',
1076
+ },
1077
+ },
1078
+ required: ['apiKey'],
1079
+ },
1080
+ },
1081
+ {
1082
+ name: 'vapi_list_assistants',
1083
+ description:
1084
+ 'List all VAPI voice assistants in your account. Shows assistant names, IDs, and configurations. Use when user asks "show my assistants", "list voice bots", or "what assistants do I have".',
1085
+ inputSchema: {
1086
+ type: 'object' as const,
1087
+ properties: {
1088
+ limit: {
1089
+ type: 'number',
1090
+ description: 'Maximum number of assistants to return (default: 20)',
1091
+ },
1092
+ },
1093
+ },
1094
+ },
1095
+ {
1096
+ name: 'vapi_create_assistant',
1097
+ description:
1098
+ 'Create a new VAPI voice assistant with CodeBakers best practices. Generates assistant with proper prompts, voice settings, and webhook configurations. Use when user says "create assistant", "new voice bot", or "setup voice agent".',
1099
+ inputSchema: {
1100
+ type: 'object' as const,
1101
+ properties: {
1102
+ name: {
1103
+ type: 'string',
1104
+ description: 'Name for the assistant',
1105
+ },
1106
+ description: {
1107
+ type: 'string',
1108
+ description: 'What the assistant should do (e.g., "Customer support for a SaaS product")',
1109
+ },
1110
+ voice: {
1111
+ type: 'string',
1112
+ enum: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'],
1113
+ description: 'Voice to use (default: alloy)',
1114
+ },
1115
+ webhookUrl: {
1116
+ type: 'string',
1117
+ description: 'Optional webhook URL for call events',
1118
+ },
1119
+ },
1120
+ required: ['name', 'description'],
1121
+ },
1122
+ },
1123
+ {
1124
+ name: 'vapi_get_assistant',
1125
+ description:
1126
+ 'Get details of a specific VAPI assistant including its configuration, prompts, and settings.',
1127
+ inputSchema: {
1128
+ type: 'object' as const,
1129
+ properties: {
1130
+ assistantId: {
1131
+ type: 'string',
1132
+ description: 'The assistant ID to retrieve',
1133
+ },
1134
+ },
1135
+ required: ['assistantId'],
1136
+ },
1137
+ },
1138
+ {
1139
+ name: 'vapi_update_assistant',
1140
+ description:
1141
+ 'Update an existing VAPI assistant. Modify prompts, voice settings, or configurations.',
1142
+ inputSchema: {
1143
+ type: 'object' as const,
1144
+ properties: {
1145
+ assistantId: {
1146
+ type: 'string',
1147
+ description: 'The assistant ID to update',
1148
+ },
1149
+ name: {
1150
+ type: 'string',
1151
+ description: 'New name for the assistant',
1152
+ },
1153
+ systemPrompt: {
1154
+ type: 'string',
1155
+ description: 'New system prompt for the assistant',
1156
+ },
1157
+ voice: {
1158
+ type: 'string',
1159
+ enum: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'],
1160
+ description: 'New voice to use',
1161
+ },
1162
+ },
1163
+ required: ['assistantId'],
1164
+ },
1165
+ },
1166
+ {
1167
+ name: 'vapi_get_calls',
1168
+ description:
1169
+ 'Get recent call logs from VAPI. Shows call duration, status, transcripts, and costs. Use when user asks "show calls", "call history", or "what calls happened".',
1170
+ inputSchema: {
1171
+ type: 'object' as const,
1172
+ properties: {
1173
+ assistantId: {
1174
+ type: 'string',
1175
+ description: 'Filter by assistant ID (optional)',
1176
+ },
1177
+ limit: {
1178
+ type: 'number',
1179
+ description: 'Maximum number of calls to return (default: 20)',
1180
+ },
1181
+ },
1182
+ },
1183
+ },
1184
+ {
1185
+ name: 'vapi_get_call',
1186
+ description:
1187
+ 'Get details of a specific call including full transcript, recording URL, and analysis.',
1188
+ inputSchema: {
1189
+ type: 'object' as const,
1190
+ properties: {
1191
+ callId: {
1192
+ type: 'string',
1193
+ description: 'The call ID to retrieve',
1194
+ },
1195
+ },
1196
+ required: ['callId'],
1197
+ },
1198
+ },
1199
+ {
1200
+ name: 'vapi_generate_webhook',
1201
+ description:
1202
+ 'Generate a VAPI webhook handler for your Next.js project. Creates API route with proper signature verification and event handling. Use when user says "add vapi webhook", "handle vapi events", or "setup call notifications".',
1203
+ inputSchema: {
1204
+ type: 'object' as const,
1205
+ properties: {
1206
+ events: {
1207
+ type: 'array',
1208
+ items: { type: 'string' },
1209
+ description: 'Events to handle: call-started, call-ended, speech-update, transcript, function-call, hang, tool-calls',
1210
+ },
1211
+ },
1212
+ },
1213
+ },
1050
1214
  ],
1051
1215
  }));
1052
1216
 
@@ -1180,6 +1344,34 @@ class CodeBakersServer {
1180
1344
  case 'update_patterns':
1181
1345
  return this.handleUpdatePatterns(args as { force?: boolean });
1182
1346
 
1347
+ case 'detect_intent':
1348
+ return this.handleDetectIntent(args as { userMessage: string });
1349
+
1350
+ // VAPI Voice AI handlers
1351
+ case 'vapi_connect':
1352
+ return this.handleVapiConnect(args as { apiKey: string });
1353
+
1354
+ case 'vapi_list_assistants':
1355
+ return this.handleVapiListAssistants(args as { limit?: number });
1356
+
1357
+ case 'vapi_create_assistant':
1358
+ return this.handleVapiCreateAssistant(args as { name: string; description: string; voice?: string; webhookUrl?: string });
1359
+
1360
+ case 'vapi_get_assistant':
1361
+ return this.handleVapiGetAssistant(args as { assistantId: string });
1362
+
1363
+ case 'vapi_update_assistant':
1364
+ return this.handleVapiUpdateAssistant(args as { assistantId: string; name?: string; systemPrompt?: string; voice?: string });
1365
+
1366
+ case 'vapi_get_calls':
1367
+ return this.handleVapiGetCalls(args as { assistantId?: string; limit?: number });
1368
+
1369
+ case 'vapi_get_call':
1370
+ return this.handleVapiGetCall(args as { callId: string });
1371
+
1372
+ case 'vapi_generate_webhook':
1373
+ return this.handleVapiGenerateWebhook(args as { events?: string[] });
1374
+
1183
1375
  default:
1184
1376
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1185
1377
  }
@@ -5303,6 +5495,637 @@ ${handlers.join('\n')}
5303
5495
  };
5304
5496
  }
5305
5497
 
5498
+ /**
5499
+ * Detect intent from user message and suggest appropriate MCP tools
5500
+ * Shows what would happen before executing, for user confirmation
5501
+ */
5502
+ private handleDetectIntent(args: { userMessage: string }) {
5503
+ const { userMessage } = args;
5504
+ const msg = userMessage.toLowerCase();
5505
+
5506
+ // Define intent patterns with their MCP tools
5507
+ const intentPatterns = [
5508
+ {
5509
+ keywords: ['upgrade codebakers', 'update patterns', 'sync patterns', 'download patterns', 'get latest patterns'],
5510
+ tool: 'update_patterns',
5511
+ description: 'Download latest CLAUDE.md and all .claude/ modules from the CodeBakers server',
5512
+ action: 'WRITE - Will overwrite local pattern files with server versions',
5513
+ isDestructive: true,
5514
+ },
5515
+ {
5516
+ keywords: ['upgrade', 'improve code', 'make production ready', 'code quality'],
5517
+ tool: 'upgrade',
5518
+ description: 'Analyze your code for quality improvements (does NOT download patterns)',
5519
+ action: 'READ-ONLY - Analyzes code and suggests improvements',
5520
+ isDestructive: false,
5521
+ },
5522
+ {
5523
+ keywords: ['build', 'create project', 'new project', 'scaffold', 'start fresh'],
5524
+ tool: 'scaffold_project',
5525
+ description: 'Create a new project from scratch with CodeBakers patterns',
5526
+ action: 'WRITE - Will create new files and folders',
5527
+ isDestructive: true,
5528
+ },
5529
+ {
5530
+ keywords: ['add feature', 'implement', 'build feature', 'create feature'],
5531
+ tool: 'optimize_and_build',
5532
+ description: 'Optimize your feature request with AI and fetch relevant patterns',
5533
+ action: 'READ + ASSIST - Fetches patterns and guides implementation',
5534
+ isDestructive: false,
5535
+ },
5536
+ {
5537
+ keywords: ['audit', 'review code', 'check code', 'code review'],
5538
+ tool: 'run_audit',
5539
+ description: 'Run comprehensive code quality audit',
5540
+ action: 'READ-ONLY - Analyzes code and reports issues',
5541
+ isDestructive: false,
5542
+ },
5543
+ {
5544
+ keywords: ['heal', 'fix errors', 'auto-fix', 'fix bugs'],
5545
+ tool: 'heal',
5546
+ description: 'AI-powered error detection and automatic fixing',
5547
+ action: 'WRITE - May modify files to fix errors',
5548
+ isDestructive: true,
5549
+ },
5550
+ {
5551
+ keywords: ['design', 'clone design', 'copy design', 'match design'],
5552
+ tool: 'design',
5553
+ description: 'Clone a design from mockups, screenshots, or websites',
5554
+ action: 'WRITE - Will generate component files',
5555
+ isDestructive: true,
5556
+ },
5557
+ {
5558
+ keywords: ['status', 'progress', "what's built", 'where am i'],
5559
+ tool: 'project_status',
5560
+ description: 'Show current project build progress and stats',
5561
+ action: 'READ-ONLY - Shows project state',
5562
+ isDestructive: false,
5563
+ },
5564
+ {
5565
+ keywords: ['run tests', 'test', 'check tests'],
5566
+ tool: 'run_tests',
5567
+ description: 'Execute the project test suite',
5568
+ action: 'READ-ONLY - Runs tests and reports results',
5569
+ isDestructive: false,
5570
+ },
5571
+ {
5572
+ keywords: ['list patterns', 'show patterns', 'what patterns'],
5573
+ tool: 'list_patterns',
5574
+ description: 'List all available CodeBakers patterns',
5575
+ action: 'READ-ONLY - Shows available patterns',
5576
+ isDestructive: false,
5577
+ },
5578
+ {
5579
+ keywords: ['init', 'initialize', 'add patterns to existing'],
5580
+ tool: 'init_project',
5581
+ description: 'Add CodeBakers patterns to an existing project',
5582
+ action: 'WRITE - Will add CLAUDE.md and .claude/ folder',
5583
+ isDestructive: true,
5584
+ },
5585
+ ];
5586
+
5587
+ // Find matching intents
5588
+ const matches = intentPatterns.filter(pattern =>
5589
+ pattern.keywords.some(keyword => msg.includes(keyword))
5590
+ );
5591
+
5592
+ let response = `# 🔍 Intent Detection\n\n`;
5593
+ response += `**Your message:** "${userMessage}"\n\n`;
5594
+
5595
+ if (matches.length === 0) {
5596
+ response += `## No specific MCP tool detected\n\n`;
5597
+ response += `Your request doesn't clearly match any MCP tool. I'll proceed with general assistance.\n\n`;
5598
+ response += `**Available tools you might want:**\n`;
5599
+ response += `- \`update_patterns\` - Download latest patterns from server\n`;
5600
+ response += `- \`optimize_and_build\` - Get AI help building a feature\n`;
5601
+ response += `- \`run_audit\` - Review your code quality\n`;
5602
+ response += `- \`project_status\` - Check build progress\n`;
5603
+ } else if (matches.length === 1) {
5604
+ const match = matches[0];
5605
+ response += `## Detected Intent\n\n`;
5606
+ response += `| Property | Value |\n`;
5607
+ response += `|----------|-------|\n`;
5608
+ response += `| **Tool** | \`${match.tool}\` |\n`;
5609
+ response += `| **Description** | ${match.description} |\n`;
5610
+ response += `| **Action Type** | ${match.action} |\n`;
5611
+ response += `| **Destructive?** | ${match.isDestructive ? '⚠️ YES - modifies files' : '✅ NO - read-only'} |\n\n`;
5612
+
5613
+ if (match.isDestructive) {
5614
+ response += `### ⚠️ Confirmation Required\n\n`;
5615
+ response += `This action will modify files. Do you want to proceed?\n\n`;
5616
+ response += `**Reply "yes" or "proceed" to execute, or describe what you actually want.**\n`;
5617
+ } else {
5618
+ response += `This is a read-only operation. Safe to proceed.\n\n`;
5619
+ response += `**Reply "yes" to execute, or clarify your request.**\n`;
5620
+ }
5621
+ } else {
5622
+ response += `## Multiple Possible Intents\n\n`;
5623
+ response += `Your request could match several tools:\n\n`;
5624
+
5625
+ matches.forEach((match, i) => {
5626
+ response += `### Option ${i + 1}: \`${match.tool}\`\n`;
5627
+ response += `- **What it does:** ${match.description}\n`;
5628
+ response += `- **Action:** ${match.action}\n`;
5629
+ response += `- **Destructive:** ${match.isDestructive ? '⚠️ Yes' : '✅ No'}\n\n`;
5630
+ });
5631
+
5632
+ response += `**Which option do you want?** Reply with the tool name or option number.\n`;
5633
+ }
5634
+
5635
+ return {
5636
+ content: [{
5637
+ type: 'text' as const,
5638
+ text: response,
5639
+ }],
5640
+ };
5641
+ }
5642
+
5643
+ // ==================== VAPI Voice AI Handlers ====================
5644
+
5645
+ private getVapiKey(): string | null {
5646
+ // Check environment variable first
5647
+ if (process.env.VAPI_API_KEY) {
5648
+ return process.env.VAPI_API_KEY;
5649
+ }
5650
+ // Check .env file in project
5651
+ const cwd = process.cwd();
5652
+ const envPath = path.join(cwd, '.env');
5653
+ if (fs.existsSync(envPath)) {
5654
+ const envContent = fs.readFileSync(envPath, 'utf-8');
5655
+ const match = envContent.match(/VAPI_API_KEY=(.+)/);
5656
+ if (match) {
5657
+ return match[1].trim();
5658
+ }
5659
+ }
5660
+ return null;
5661
+ }
5662
+
5663
+ private async handleVapiConnect(args: { apiKey: string }) {
5664
+ const { apiKey } = args;
5665
+ const cwd = process.cwd();
5666
+
5667
+ let response = `# 🎙️ VAPI Connection Setup\n\n`;
5668
+
5669
+ try {
5670
+ // Test the API key
5671
+ const testResponse = await fetch('https://api.vapi.ai/assistant', {
5672
+ method: 'GET',
5673
+ headers: {
5674
+ 'Authorization': `Bearer ${apiKey}`,
5675
+ },
5676
+ });
5677
+
5678
+ if (!testResponse.ok) {
5679
+ response += `## ❌ Invalid API Key\n\n`;
5680
+ response += `The API key could not be validated. Please check:\n`;
5681
+ response += `1. Go to https://dashboard.vapi.ai\n`;
5682
+ response += `2. Navigate to Settings → API Keys\n`;
5683
+ response += `3. Copy your API key and try again\n`;
5684
+ return { content: [{ type: 'text' as const, text: response }] };
5685
+ }
5686
+
5687
+ // Save to .env file
5688
+ const envPath = path.join(cwd, '.env');
5689
+ let envContent = '';
5690
+
5691
+ if (fs.existsSync(envPath)) {
5692
+ envContent = fs.readFileSync(envPath, 'utf-8');
5693
+ if (envContent.includes('VAPI_API_KEY=')) {
5694
+ envContent = envContent.replace(/VAPI_API_KEY=.*/g, `VAPI_API_KEY=${apiKey}`);
5695
+ } else {
5696
+ envContent += `\n# VAPI Voice AI\nVAPI_API_KEY=${apiKey}\n`;
5697
+ }
5698
+ } else {
5699
+ envContent = `# VAPI Voice AI\nVAPI_API_KEY=${apiKey}\n`;
5700
+ }
5701
+
5702
+ fs.writeFileSync(envPath, envContent);
5703
+
5704
+ response += `## ✅ VAPI Connected Successfully!\n\n`;
5705
+ response += `API key saved to \`.env\` file.\n\n`;
5706
+ response += `### Available Commands:\n`;
5707
+ response += `- \`vapi_list_assistants\` - See your assistants\n`;
5708
+ response += `- \`vapi_create_assistant\` - Create a new voice assistant\n`;
5709
+ response += `- \`vapi_get_calls\` - View call history\n`;
5710
+ response += `- \`vapi_generate_webhook\` - Add webhook handler to your project\n`;
5711
+
5712
+ } catch (error) {
5713
+ const message = error instanceof Error ? error.message : 'Unknown error';
5714
+ response += `## ❌ Connection Failed\n\n`;
5715
+ response += `Error: ${message}\n`;
5716
+ }
5717
+
5718
+ return { content: [{ type: 'text' as const, text: response }] };
5719
+ }
5720
+
5721
+ private async handleVapiListAssistants(args: { limit?: number }) {
5722
+ const { limit = 20 } = args;
5723
+ const vapiKey = this.getVapiKey();
5724
+
5725
+ let response = `# 🎙️ VAPI Assistants\n\n`;
5726
+
5727
+ if (!vapiKey) {
5728
+ response += `## ❌ Not Connected\n\n`;
5729
+ response += `VAPI API key not found. Run \`vapi_connect\` first with your API key.\n`;
5730
+ response += `Get your key at: https://dashboard.vapi.ai\n`;
5731
+ return { content: [{ type: 'text' as const, text: response }] };
5732
+ }
5733
+
5734
+ try {
5735
+ const apiResponse = await fetch(`https://api.vapi.ai/assistant?limit=${limit}`, {
5736
+ method: 'GET',
5737
+ headers: {
5738
+ 'Authorization': `Bearer ${vapiKey}`,
5739
+ },
5740
+ });
5741
+
5742
+ if (!apiResponse.ok) {
5743
+ throw new Error(`API error: ${apiResponse.status}`);
5744
+ }
5745
+
5746
+ const assistants = await apiResponse.json();
5747
+
5748
+ if (!assistants || assistants.length === 0) {
5749
+ response += `No assistants found.\n\n`;
5750
+ response += `Create one with \`vapi_create_assistant\`!\n`;
5751
+ } else {
5752
+ response += `| Name | ID | Voice | Created |\n`;
5753
+ response += `|------|-------|-------|--------|\n`;
5754
+
5755
+ for (const a of assistants) {
5756
+ const created = new Date(a.createdAt).toLocaleDateString();
5757
+ const voice = a.voice?.provider || 'default';
5758
+ response += `| ${a.name || 'Unnamed'} | \`${a.id.slice(0, 8)}...\` | ${voice} | ${created} |\n`;
5759
+ }
5760
+
5761
+ response += `\n**Total:** ${assistants.length} assistant(s)\n`;
5762
+ }
5763
+
5764
+ } catch (error) {
5765
+ const message = error instanceof Error ? error.message : 'Unknown error';
5766
+ response += `## ❌ Error\n\n`;
5767
+ response += `Failed to fetch assistants: ${message}\n`;
5768
+ }
5769
+
5770
+ return { content: [{ type: 'text' as const, text: response }] };
5771
+ }
5772
+
5773
+ private async handleVapiCreateAssistant(args: { name: string; description: string; voice?: string; webhookUrl?: string }) {
5774
+ const { name, description, voice = 'alloy', webhookUrl } = args;
5775
+ const vapiKey = this.getVapiKey();
5776
+
5777
+ let response = `# 🎙️ Create VAPI Assistant\n\n`;
5778
+
5779
+ if (!vapiKey) {
5780
+ response += `## ❌ Not Connected\n\n`;
5781
+ response += `VAPI API key not found. Run \`vapi_connect\` first.\n`;
5782
+ return { content: [{ type: 'text' as const, text: response }] };
5783
+ }
5784
+
5785
+ try {
5786
+ // Build system prompt based on description
5787
+ const systemPrompt = `You are ${name}, a helpful voice assistant. ${description}
5788
+
5789
+ Guidelines:
5790
+ - Be conversational and natural
5791
+ - Keep responses concise (1-2 sentences when possible)
5792
+ - Ask clarifying questions if needed
5793
+ - Be friendly but professional`;
5794
+
5795
+ const assistantConfig: Record<string, unknown> = {
5796
+ name,
5797
+ model: {
5798
+ provider: 'openai',
5799
+ model: 'gpt-4o-mini',
5800
+ systemPrompt,
5801
+ },
5802
+ voice: {
5803
+ provider: 'openai',
5804
+ voiceId: voice,
5805
+ },
5806
+ firstMessage: `Hi! I'm ${name}. How can I help you today?`,
5807
+ };
5808
+
5809
+ if (webhookUrl) {
5810
+ assistantConfig.serverUrl = webhookUrl;
5811
+ }
5812
+
5813
+ const apiResponse = await fetch('https://api.vapi.ai/assistant', {
5814
+ method: 'POST',
5815
+ headers: {
5816
+ 'Authorization': `Bearer ${vapiKey}`,
5817
+ 'Content-Type': 'application/json',
5818
+ },
5819
+ body: JSON.stringify(assistantConfig),
5820
+ });
5821
+
5822
+ if (!apiResponse.ok) {
5823
+ const error = await apiResponse.json();
5824
+ throw new Error(error.message || `API error: ${apiResponse.status}`);
5825
+ }
5826
+
5827
+ const assistant = await apiResponse.json();
5828
+
5829
+ response += `## ✅ Assistant Created!\n\n`;
5830
+ response += `| Property | Value |\n`;
5831
+ response += `|----------|-------|\n`;
5832
+ response += `| **Name** | ${assistant.name} |\n`;
5833
+ response += `| **ID** | \`${assistant.id}\` |\n`;
5834
+ response += `| **Voice** | ${voice} |\n`;
5835
+ response += `| **Model** | gpt-4o-mini |\n`;
5836
+
5837
+ response += `\n### Next Steps:\n`;
5838
+ response += `1. Test your assistant at https://dashboard.vapi.ai\n`;
5839
+ response += `2. Add a phone number to receive calls\n`;
5840
+ response += `3. Use \`vapi_generate_webhook\` to handle call events\n`;
5841
+
5842
+ } catch (error) {
5843
+ const message = error instanceof Error ? error.message : 'Unknown error';
5844
+ response += `## ❌ Creation Failed\n\n`;
5845
+ response += `Error: ${message}\n`;
5846
+ }
5847
+
5848
+ return { content: [{ type: 'text' as const, text: response }] };
5849
+ }
5850
+
5851
+ private async handleVapiGetAssistant(args: { assistantId: string }) {
5852
+ const { assistantId } = args;
5853
+ const vapiKey = this.getVapiKey();
5854
+
5855
+ let response = `# 🎙️ Assistant Details\n\n`;
5856
+
5857
+ if (!vapiKey) {
5858
+ response += `## ❌ Not Connected\n\nRun \`vapi_connect\` first.\n`;
5859
+ return { content: [{ type: 'text' as const, text: response }] };
5860
+ }
5861
+
5862
+ try {
5863
+ const apiResponse = await fetch(`https://api.vapi.ai/assistant/${assistantId}`, {
5864
+ headers: { 'Authorization': `Bearer ${vapiKey}` },
5865
+ });
5866
+
5867
+ if (!apiResponse.ok) {
5868
+ throw new Error(`API error: ${apiResponse.status}`);
5869
+ }
5870
+
5871
+ const a = await apiResponse.json();
5872
+
5873
+ response += `## ${a.name || 'Unnamed Assistant'}\n\n`;
5874
+ response += `| Property | Value |\n`;
5875
+ response += `|----------|-------|\n`;
5876
+ response += `| **ID** | \`${a.id}\` |\n`;
5877
+ response += `| **Voice** | ${a.voice?.voiceId || 'default'} |\n`;
5878
+ response += `| **Model** | ${a.model?.model || 'unknown'} |\n`;
5879
+ response += `| **Created** | ${new Date(a.createdAt).toLocaleString()} |\n`;
5880
+
5881
+ if (a.model?.systemPrompt) {
5882
+ response += `\n### System Prompt:\n\`\`\`\n${a.model.systemPrompt.slice(0, 500)}${a.model.systemPrompt.length > 500 ? '...' : ''}\n\`\`\`\n`;
5883
+ }
5884
+
5885
+ } catch (error) {
5886
+ const message = error instanceof Error ? error.message : 'Unknown error';
5887
+ response += `## ❌ Error\n\n${message}\n`;
5888
+ }
5889
+
5890
+ return { content: [{ type: 'text' as const, text: response }] };
5891
+ }
5892
+
5893
+ private async handleVapiUpdateAssistant(args: { assistantId: string; name?: string; systemPrompt?: string; voice?: string }) {
5894
+ const { assistantId, name, systemPrompt, voice } = args;
5895
+ const vapiKey = this.getVapiKey();
5896
+
5897
+ let response = `# 🎙️ Update Assistant\n\n`;
5898
+
5899
+ if (!vapiKey) {
5900
+ response += `## ❌ Not Connected\n\nRun \`vapi_connect\` first.\n`;
5901
+ return { content: [{ type: 'text' as const, text: response }] };
5902
+ }
5903
+
5904
+ try {
5905
+ const updates: Record<string, unknown> = {};
5906
+ if (name) updates.name = name;
5907
+ if (systemPrompt) updates.model = { systemPrompt };
5908
+ if (voice) updates.voice = { provider: 'openai', voiceId: voice };
5909
+
5910
+ const apiResponse = await fetch(`https://api.vapi.ai/assistant/${assistantId}`, {
5911
+ method: 'PATCH',
5912
+ headers: {
5913
+ 'Authorization': `Bearer ${vapiKey}`,
5914
+ 'Content-Type': 'application/json',
5915
+ },
5916
+ body: JSON.stringify(updates),
5917
+ });
5918
+
5919
+ if (!apiResponse.ok) {
5920
+ throw new Error(`API error: ${apiResponse.status}`);
5921
+ }
5922
+
5923
+ response += `## ✅ Assistant Updated!\n\n`;
5924
+ response += `Updated fields:\n`;
5925
+ if (name) response += `- Name: ${name}\n`;
5926
+ if (systemPrompt) response += `- System prompt updated\n`;
5927
+ if (voice) response += `- Voice: ${voice}\n`;
5928
+
5929
+ } catch (error) {
5930
+ const message = error instanceof Error ? error.message : 'Unknown error';
5931
+ response += `## ❌ Update Failed\n\n${message}\n`;
5932
+ }
5933
+
5934
+ return { content: [{ type: 'text' as const, text: response }] };
5935
+ }
5936
+
5937
+ private async handleVapiGetCalls(args: { assistantId?: string; limit?: number }) {
5938
+ const { assistantId, limit = 20 } = args;
5939
+ const vapiKey = this.getVapiKey();
5940
+
5941
+ let response = `# 🎙️ VAPI Call History\n\n`;
5942
+
5943
+ if (!vapiKey) {
5944
+ response += `## ❌ Not Connected\n\nRun \`vapi_connect\` first.\n`;
5945
+ return { content: [{ type: 'text' as const, text: response }] };
5946
+ }
5947
+
5948
+ try {
5949
+ let url = `https://api.vapi.ai/call?limit=${limit}`;
5950
+ if (assistantId) url += `&assistantId=${assistantId}`;
5951
+
5952
+ const apiResponse = await fetch(url, {
5953
+ headers: { 'Authorization': `Bearer ${vapiKey}` },
5954
+ });
5955
+
5956
+ if (!apiResponse.ok) {
5957
+ throw new Error(`API error: ${apiResponse.status}`);
5958
+ }
5959
+
5960
+ const calls = await apiResponse.json();
5961
+
5962
+ if (!calls || calls.length === 0) {
5963
+ response += `No calls found.\n`;
5964
+ } else {
5965
+ response += `| Date | Duration | Status | Cost |\n`;
5966
+ response += `|------|----------|--------|------|\n`;
5967
+
5968
+ for (const call of calls) {
5969
+ const date = new Date(call.createdAt).toLocaleString();
5970
+ const duration = call.endedAt ? Math.round((new Date(call.endedAt).getTime() - new Date(call.startedAt).getTime()) / 1000) : 0;
5971
+ const cost = call.cost ? `$${call.cost.toFixed(3)}` : '-';
5972
+ response += `| ${date} | ${duration}s | ${call.status} | ${cost} |\n`;
5973
+ }
5974
+
5975
+ response += `\n**Total:** ${calls.length} call(s)\n`;
5976
+ }
5977
+
5978
+ } catch (error) {
5979
+ const message = error instanceof Error ? error.message : 'Unknown error';
5980
+ response += `## ❌ Error\n\n${message}\n`;
5981
+ }
5982
+
5983
+ return { content: [{ type: 'text' as const, text: response }] };
5984
+ }
5985
+
5986
+ private async handleVapiGetCall(args: { callId: string }) {
5987
+ const { callId } = args;
5988
+ const vapiKey = this.getVapiKey();
5989
+
5990
+ let response = `# 🎙️ Call Details\n\n`;
5991
+
5992
+ if (!vapiKey) {
5993
+ response += `## ❌ Not Connected\n\nRun \`vapi_connect\` first.\n`;
5994
+ return { content: [{ type: 'text' as const, text: response }] };
5995
+ }
5996
+
5997
+ try {
5998
+ const apiResponse = await fetch(`https://api.vapi.ai/call/${callId}`, {
5999
+ headers: { 'Authorization': `Bearer ${vapiKey}` },
6000
+ });
6001
+
6002
+ if (!apiResponse.ok) {
6003
+ throw new Error(`API error: ${apiResponse.status}`);
6004
+ }
6005
+
6006
+ const call = await apiResponse.json();
6007
+
6008
+ response += `| Property | Value |\n`;
6009
+ response += `|----------|-------|\n`;
6010
+ response += `| **ID** | \`${call.id}\` |\n`;
6011
+ response += `| **Status** | ${call.status} |\n`;
6012
+ response += `| **Started** | ${new Date(call.startedAt).toLocaleString()} |\n`;
6013
+ if (call.endedAt) {
6014
+ const duration = Math.round((new Date(call.endedAt).getTime() - new Date(call.startedAt).getTime()) / 1000);
6015
+ response += `| **Duration** | ${duration} seconds |\n`;
6016
+ }
6017
+ if (call.cost) response += `| **Cost** | $${call.cost.toFixed(4)} |\n`;
6018
+ if (call.recordingUrl) response += `| **Recording** | [Listen](${call.recordingUrl}) |\n`;
6019
+
6020
+ if (call.transcript) {
6021
+ response += `\n### Transcript:\n\`\`\`\n${call.transcript.slice(0, 1000)}${call.transcript.length > 1000 ? '...' : ''}\n\`\`\`\n`;
6022
+ }
6023
+
6024
+ } catch (error) {
6025
+ const message = error instanceof Error ? error.message : 'Unknown error';
6026
+ response += `## ❌ Error\n\n${message}\n`;
6027
+ }
6028
+
6029
+ return { content: [{ type: 'text' as const, text: response }] };
6030
+ }
6031
+
6032
+ private handleVapiGenerateWebhook(args: { events?: string[] }) {
6033
+ const { events = ['call-started', 'call-ended', 'transcript'] } = args;
6034
+ const cwd = process.cwd();
6035
+
6036
+ let response = `# 🎙️ VAPI Webhook Generator\n\n`;
6037
+
6038
+ // Generate webhook handler code
6039
+ const webhookCode = `import { NextRequest, NextResponse } from 'next/server';
6040
+
6041
+ // VAPI Webhook Event Types
6042
+ type VapiEventType = ${events.map(e => `'${e}'`).join(' | ')};
6043
+
6044
+ interface VapiWebhookPayload {
6045
+ type: VapiEventType;
6046
+ call?: {
6047
+ id: string;
6048
+ assistantId: string;
6049
+ phoneNumber?: string;
6050
+ customer?: {
6051
+ number: string;
6052
+ name?: string;
6053
+ };
6054
+ };
6055
+ transcript?: string;
6056
+ timestamp: string;
6057
+ }
6058
+
6059
+ export async function POST(req: NextRequest) {
6060
+ try {
6061
+ const payload: VapiWebhookPayload = await req.json();
6062
+
6063
+ console.log('[VAPI Webhook]', payload.type, payload.call?.id);
6064
+
6065
+ switch (payload.type) {
6066
+ ${events.includes('call-started') ? ` case 'call-started':
6067
+ // Handle call started
6068
+ // Example: Log to database, send notification
6069
+ console.log('Call started:', payload.call?.id);
6070
+ break;
6071
+ ` : ''}${events.includes('call-ended') ? ` case 'call-ended':
6072
+ // Handle call ended
6073
+ // Example: Save transcript, update CRM
6074
+ console.log('Call ended:', payload.call?.id);
6075
+ break;
6076
+ ` : ''}${events.includes('transcript') ? ` case 'transcript':
6077
+ // Handle real-time transcript updates
6078
+ console.log('Transcript:', payload.transcript);
6079
+ break;
6080
+ ` : ''}${events.includes('function-call') ? ` case 'function-call':
6081
+ // Handle function calls from assistant
6082
+ // Return data to be spoken by the assistant
6083
+ return NextResponse.json({ result: 'Function executed' });
6084
+ ` : ''} default:
6085
+ console.log('Unhandled event:', payload.type);
6086
+ }
6087
+
6088
+ return NextResponse.json({ received: true });
6089
+ } catch (error) {
6090
+ console.error('[VAPI Webhook Error]', error);
6091
+ return NextResponse.json({ error: 'Webhook error' }, { status: 400 });
6092
+ }
6093
+ }
6094
+ `;
6095
+
6096
+ // Check if API route directory exists
6097
+ const apiDir = path.join(cwd, 'src', 'app', 'api', 'webhooks', 'vapi');
6098
+ const routePath = path.join(apiDir, 'route.ts');
6099
+
6100
+ if (fs.existsSync(routePath)) {
6101
+ response += `## ⚠️ Webhook Already Exists\n\n`;
6102
+ response += `File: \`src/app/api/webhooks/vapi/route.ts\`\n\n`;
6103
+ response += `Here's the updated code if you want to replace it:\n\n`;
6104
+ } else {
6105
+ // Create directory and file
6106
+ fs.mkdirSync(apiDir, { recursive: true });
6107
+ fs.writeFileSync(routePath, webhookCode);
6108
+ response += `## ✅ Webhook Created!\n\n`;
6109
+ response += `File: \`src/app/api/webhooks/vapi/route.ts\`\n\n`;
6110
+ }
6111
+
6112
+ response += `### Generated Code:\n\`\`\`typescript\n${webhookCode}\n\`\`\`\n\n`;
6113
+
6114
+ response += `### Setup Instructions:\n`;
6115
+ response += `1. Deploy your app to get a public URL\n`;
6116
+ response += `2. Go to https://dashboard.vapi.ai → Your Assistant → Settings\n`;
6117
+ response += `3. Set webhook URL to: \`https://your-domain.com/api/webhooks/vapi\`\n`;
6118
+ response += `4. Select events: ${events.join(', ')}\n\n`;
6119
+
6120
+ response += `### Handling Events:\n`;
6121
+ response += `- \`call-started\`: Triggered when a call begins\n`;
6122
+ response += `- \`call-ended\`: Triggered when a call ends (with transcript)\n`;
6123
+ response += `- \`transcript\`: Real-time transcript updates\n`;
6124
+ response += `- \`function-call\`: When assistant calls a custom function\n`;
6125
+
6126
+ return { content: [{ type: 'text' as const, text: response }] };
6127
+ }
6128
+
5306
6129
  async run(): Promise<void> {
5307
6130
  const transport = new StdioServerTransport();
5308
6131
  await this.server.connect(transport);