UNPKG

c9ai

Version:

Universal AI assistant with vibe-based workflows, hybrid cloud+local AI, and comprehensive tool integration

605 lines (533 loc) 22.2 kB
"use strict"; /** * Sigil Router - Routes @commands to appropriate JIT applications */ class SigilRouter { constructor() { // Sigil mappings to JIT tool configurations this.sigilMap = { // Calculator sigils '@calc': { tool: 'jit', params: { type: 'calc' } }, '@calculate': { tool: 'jit', params: { type: 'calc' } }, '@math': { tool: 'jit', params: { type: 'calc' } }, // Data analysis sigils '@analyze': { tool: 'jit', params: { type: 'analyze' } }, '@data': { tool: 'jit', params: { type: 'analyze' } }, '@csv': { tool: 'jit', params: { type: 'analyze' } }, // File processing sigils '@file': { tool: 'jit', params: { type: 'file' } }, '@process': { tool: 'jit', params: { type: 'file' } }, '@count': { tool: 'jit', params: { type: 'file', operation: 'count-lines' } }, '@upper': { tool: 'jit', params: { type: 'file', operation: 'uppercase' } }, '@lower': { tool: 'jit', params: { type: 'file', operation: 'lowercase' } }, '@emails': { tool: 'jit', params: { type: 'file', operation: 'extract-emails' } }, '@words': { tool: 'jit', params: { type: 'file', operation: 'word-frequency' } }, // System info sigils '@system': { tool: 'jit', params: { type: 'system' } }, // Function inspection sigils '@inspect': { tool: 'jit', params: { type: 'inspect' } }, '@show': { tool: 'jit', params: { type: 'inspect' } }, '@function': { tool: 'jit', params: { type: 'inspect' } }, '@info': { tool: 'jit', params: { type: 'info' } }, '@status': { tool: 'jit', params: { type: 'system' } }, // XML-Lisp development sigils '@transpile': { tool: 'jit', params: { type: 'transpile' } }, '@xml': { tool: 'jit', params: { type: 'transpile' } }, '@xmljs': { tool: 'jit', params: { type: 'transpile' } }, '@create': { tool: 'jit', params: { type: 'ai_create_function' } }, '@generate': { tool: 'jit', params: { type: 'ai_create_function' } }, '@executive': { tool: 'jit', params: { type: 'executive_request' } }, '@business': { tool: 'jit', params: { type: 'executive_request' } }, // Common alias for executives '@exec': { tool: 'jit', params: { type: 'executive_request' } }, '@prog': { tool: 'jit', params: { type: 'prog' } }, '@program': { tool: 'jit', params: { type: 'prog' } }, '@basic': { tool: 'jit', params: { type: 'prog' } }, '@lang': { tool: 'jit', params: { type: 'lang' } }, '@language': { tool: 'jit', params: { type: 'lang' } }, '@save': { tool: 'jit', params: { type: 'save' } }, '@run': { tool: 'jit', params: { type: 'run' } }, '@execute': { tool: 'jit', params: { type: 'run' } }, // Quiz generation sigils '@quiz': { tool: 'jit', params: { type: 'quiz' } }, '@test': { tool: 'jit', params: { type: 'quiz' } }, '@exam': { tool: 'jit', params: { type: 'quiz' } }, // RFQ Analysis sigils '@rfq': { tool: 'jit', params: { type: 'rfq' } }, '@rfq-analysis': { tool: 'jit', params: { type: 'rfq' } }, '@proposal': { tool: 'jit', params: { type: 'rfq' } }, '@bid': { tool: 'jit', params: { type: 'rfq' } }, // Task/Todo sigils (route to existing tools) '@todo': { tool: 'shell.run', params: {} }, '@task': { tool: 'shell.run', params: {} }, '@run': { tool: 'shell.run', params: {} }, '@shell': { tool: 'shell.run', params: {} }, // Communication sigils (route to existing tools) '@email': { tool: 'cream.mail', params: {} }, '@post': { tool: 'cream.post', params: {} }, '@share': { tool: 'cream.post', params: {} }, '@publish': { tool: 'cream.post', params: {} }, '@whatsapp': { tool: 'whatsapp.send', params: {} }, '@sms': { tool: 'whatsapp.send', params: {} }, // File operations (route to existing tools) '@read': { tool: 'fs.read', params: {} }, '@write': { tool: 'fs.write', params: {} }, '@create': { tool: 'fs.write', params: {} }, // Web operations '@search': { tool: 'web.search', params: {} }, '@google': { tool: 'web.search', params: {} }, // Cream & RSS helpers '@cream.fetch': { tool: 'cream.fetch', params: {} }, '@rss': { tool: 'rss.read', params: {} }, // Development tools '@compile': { tool: 'tex.compile', params: {} }, '@tex': { tool: 'tex.compile', params: {} }, '@latex': { tool: 'tex.compile', params: {} }, '@pdf': { tool: 'tex.compile', params: {} }, // GitHub operations '@github': { tool: 'github.fetch', params: { action: 'list' } }, '@github-fetch': { tool: 'github.fetch', params: { action: 'fetch' } }, '@github-list': { tool: 'github.fetch', params: { action: 'list' } }, '@github-run': { tool: 'github.fetch', params: { action: 'execute' } }, '@issue': { tool: 'gh.issues.create', params: {} }, // Google Drive operations '@gdrive': { tool: 'gdrive.fetch', params: { action: 'list' } }, '@gdrive-fetch': { tool: 'gdrive.fetch', params: { action: 'fetch' } }, '@gdrive-list': { tool: 'gdrive.fetch', params: { action: 'list' } }, '@gdrive-run': { tool: 'gdrive.fetch', params: { action: 'execute' } }, '@drive': { tool: 'gdrive.fetch', params: { action: 'list' } }, // Media processing '@image': { tool: 'image.convert', params: {} }, '@video': { tool: 'ffmpeg.run', params: {} }, '@convert': { tool: 'image.convert', params: {} }, '@resize': { tool: 'image.convert', params: {} }, }; } /** * Check if a prompt starts with a sigil */ hasSigil(prompt) { const trimmed = prompt.trim(); return trimmed.startsWith('@'); } /** * Extract sigil and arguments from prompt */ parseSigil(prompt) { const trimmed = prompt.trim(); if (!trimmed.startsWith('@')) { return null; } // For multiline commands like @executive, preserve the structure const firstLineMatch = trimmed.match(/^(@[^\s\n]+)(.*)$/s); if (firstLineMatch) { const sigil = firstLineMatch[1].toLowerCase(); const args = firstLineMatch[2].trim(); return { sigil, args }; } // Fallback to space-based parsing const parts = trimmed.split(/\s+/); const sigil = parts[0].toLowerCase(); const args = parts.slice(1).join(' '); return { sigil, args }; } /** * Route sigil to appropriate tool call */ routeSigil(prompt) { const parsed = this.parseSigil(prompt); if (!parsed) { return null; } const { sigil, args } = parsed; const mapping = this.sigilMap[sigil]; if (!mapping) { return { error: true, message: `Unknown sigil: ${sigil}. Available sigils: ${Object.keys(this.sigilMap).slice(0, 10).join(', ')}...` }; } // Build tool call based on mapping const toolCall = { tool: mapping.tool, args: { ...mapping.params } }; // Add arguments based on tool type if (mapping.tool === 'jit') { this.addJITArgs(toolCall, mapping.params.type, args, prompt); } else { this.addStandardArgs(toolCall, mapping.tool, args); } return { success: true, sigil, toolCall, originalPrompt: prompt }; } /** * Add arguments for JIT tools */ addJITArgs(toolCall, jitType, args, originalPrompt = null) { switch (jitType) { case 'calc': case 'calculator': toolCall.args.expression = args; break; case 'analyze': case 'data': toolCall.args.file = args; break; case 'file': case 'process': // Parse file operation arguments const parts = args.split(/\s+/); if (parts.length >= 1) { toolCall.args.input = parts[0]; } if (parts.length >= 2) { toolCall.args.output = parts[1]; } break; case 'system': case 'info': // No additional args needed for system info break; case 'inspect': // Function inspection: @inspect functionName toolCall.args.function = args; break; case 'transpile': // XML-Lisp transpilation: @transpile <xml-function> toolCall.args.xml = args; break; case 'executive_request': // Executive request processing: @executive <business request> // Preserve the full structured format for multiline queries if (originalPrompt && originalPrompt.includes('\n') && originalPrompt.includes('- ')) { // For structured queries, pass the entire original prompt to preserve format toolCall.args.request = originalPrompt; } else { // For single line queries, reconstruct with sigil toolCall.args.request = `@executive ${args}`; } break; case 'prog': case 'program': // BASIC program execution: @prog <basic code> // Support multiline BASIC programs if (originalPrompt && (originalPrompt.includes('\n') || originalPrompt.includes('LET ') || originalPrompt.includes('RETURN '))) { // Extract BASIC code from the full prompt const progMatch = originalPrompt.match(/@prog[ram]*\s*([\s\S]*)/i); if (progMatch && progMatch[1]) { toolCall.args.code = progMatch[1].trim(); } else { toolCall.args.code = args; } } else { toolCall.args.code = args; } break; case 'lang': case 'language': // Language code generation: @lang python: create QR generator // Format: @lang <language>: <task> const langMatch = args.match(/^(\w+):\s*(.+)$/); if (langMatch) { toolCall.args.language = langMatch[1]; toolCall.args.task = langMatch[2]; } else { // Improved fallback: detect language keywords in the text const supportedLanguages = ['python', 'javascript', 'js', 'bash', 'sh', 'powershell', 'ps1', 'java', 'go', 'rust', 'c', 'cpp']; let detectedLanguage = null; let taskText = args; // Look for language keywords anywhere in the text for (const lang of supportedLanguages) { const langRegex = new RegExp(`\\b${lang}\\b`, 'i'); if (langRegex.test(args)) { detectedLanguage = lang; // Remove the language keyword from task text taskText = args.replace(langRegex, '').replace(/\s+/g, ' ').trim(); break; } } // If no language detected, check if first word is a language const langParts = args.split(/\s+/); if (!detectedLanguage && langParts.length >= 2 && supportedLanguages.includes(langParts[0].toLowerCase())) { detectedLanguage = langParts[0].toLowerCase(); taskText = langParts.slice(1).join(' '); } toolCall.args.language = detectedLanguage || 'javascript'; // default toolCall.args.task = taskText; } break; case 'save': case 'save_code': // Save AI-generated code: @save <ai_response_text> toolCall.args.response = args; break; case 'run': case 'execute': // Execute saved script: @run script_name.py toolCall.args.filename = args; break; case 'quiz': case 'test': case 'exam': // Parse quiz arguments: @quiz frontend intermediate 5 const quizParts = args.split(/\s+/); if (quizParts.length >= 1 && quizParts[0]) { toolCall.args.topic = quizParts[0]; } if (quizParts.length >= 2 && quizParts[1]) { toolCall.args.difficulty = quizParts[1]; } if (quizParts.length >= 3 && quizParts[2] && !isNaN(quizParts[2])) { toolCall.args.questions = parseInt(quizParts[2]); } break; case 'rfq': case 'rfq-analysis': case 'proposal': case 'bid': // Parse RFQ arguments: // @rfq "RFQ text here" rate=150 margin=20 // @rfq file="./rfq.pdf" rate=150 margin=20 // Check for file parameter first const fileMatch = args.match(/file=["']([^"']+)["']\s*(.*)/); if (fileMatch) { toolCall.args.file_path = fileMatch[1]; const params = fileMatch[2]; // Parse rate parameter const rateMatch = params.match(/rate=(\d+)/); if (rateMatch) { toolCall.args.hourly_rate = parseInt(rateMatch[1]); } // Parse margin parameter const marginMatch = params.match(/margin=(\d+)/); if (marginMatch) { toolCall.args.margin_target = parseInt(marginMatch[1]); } } else { // Handle quoted text format const rfqMatch = args.match(/^"([^"]+)"\s*(.*)/); if (rfqMatch) { toolCall.args.rfq_text = rfqMatch[1]; const params = rfqMatch[2]; // Parse rate parameter const rateMatch = params.match(/rate=(\d+)/); if (rateMatch) { toolCall.args.hourly_rate = parseInt(rateMatch[1]); } // Parse margin parameter const marginMatch = params.match(/margin=(\d+)/); if (marginMatch) { toolCall.args.margin_target = parseInt(marginMatch[1]); } } else if (args.trim()) { // If no quotes, treat entire args as RFQ text toolCall.args.rfq_text = args; } } break; } } /** * Add arguments for standard tools */ addStandardArgs(toolCall, toolName, args) { switch (toolName) { case 'shell.run': toolCall.args.command = args; break; case 'fs.read': toolCall.args.path = args; break; case 'fs.write': const parts = args.split(' -> '); if (parts.length === 2) { toolCall.args.path = parts[0]; toolCall.args.content = parts[1]; } else { toolCall.args.path = args; toolCall.args.content = ''; // Will need to be filled by AI } break; case 'web.search': toolCall.args.q = args; break; case 'cream.mail': // Parse email format: @email user@example.com subject: Subject text content: Body text // Also support: @email "user@example.com" "Subject" "Body" const structuredMatch = args.match(/^['"]*([^'"@]+@[^'"\s]+)['"]*\s+subject:\s*(.+?)\s+(?:mail\s+)?content:\s*(.+)$/i); const quotedMatch = args.match(/^['"]*([^'"@]+@[^'"\s]+)['"]*\s+"([^"]+)"\s+"([^"]+)"$/); if (structuredMatch) { toolCall.args.to_email = structuredMatch[1].replace(/['"]/g, ''); toolCall.args.subject = structuredMatch[2].trim(); toolCall.args.body = structuredMatch[3].trim(); toolCall.args.from_email = 'noreply@knoblycream.com'; // Default sender toolCall.args.from_name = 'C9AI Assistant'; } else if (quotedMatch) { toolCall.args.to_email = quotedMatch[1].replace(/['"]/g, ''); toolCall.args.subject = quotedMatch[2]; toolCall.args.body = quotedMatch[3]; toolCall.args.from_email = 'noreply@knoblycream.com'; toolCall.args.from_name = 'C9AI Assistant'; } else { // Fallback - just set the recipient, let the system prompt for the rest const emailAddr = args.match(/['"]*([^'"@]+@[^'"\s]+)['"]*/); if (emailAddr) { toolCall.args.to_email = emailAddr[1].replace(/['"]/g, ''); toolCall.args.subject = 'Message from C9AI'; toolCall.args.body = args.replace(emailAddr[0], '').trim() || 'Hello from C9AI!'; toolCall.args.from_email = 'noreply@knoblycream.com'; toolCall.args.from_name = 'C9AI Assistant'; } } break; case 'cream.post': // Parse post format: @post "Post content here" visibility:public // Also support: @post "Content" private const postContentMatch = args.match(/^"([^"]+)"\s*(?:visibility:)?(public|private)?/i); const simplePostMatch = args.match(/^"([^"]+)"\s+(public|private)/i); if (postContentMatch) { toolCall.args.content = postContentMatch[1]; toolCall.args.visibility = postContentMatch[2] || 'public'; } else if (simplePostMatch) { toolCall.args.content = simplePostMatch[1]; toolCall.args.visibility = simplePostMatch[2]; } else { // Fallback - treat entire args as content const cleanContent = args.replace(/^["']|["']$/g, ''); // Remove quotes toolCall.args.content = cleanContent || 'Hello from C9AI!'; toolCall.args.visibility = 'public'; } // Media files would need to be handled separately toolCall.args.media = []; break; case 'mail.send': // Parse email format: @email user@example.com "Subject" "Body" const legacyEmailMatch = args.match(/^(\S+)\s+"([^"]+)"\s+"([^"]+)"$/); if (legacyEmailMatch) { toolCall.args.to = legacyEmailMatch[1]; toolCall.args.subject = legacyEmailMatch[2]; toolCall.args.text = legacyEmailMatch[3]; } else { toolCall.args.to = args; // Let AI figure out the rest } break; case 'whatsapp.send': const whatsappMatch = args.match(/^(\S+)\s+"([^"]+)"$/); if (whatsappMatch) { toolCall.args.to = whatsappMatch[1]; toolCall.args.body = whatsappMatch[2]; } else { toolCall.args.to = args; } break; case 'tex.compile': toolCall.args.mainFile = args; break; case 'image.convert': const imageParts = args.split(/\s+/); if (imageParts.length >= 2) { toolCall.args.input = imageParts[0]; toolCall.args.output = imageParts[1]; } else { toolCall.args.input = args; } break; case 'github.fetch': // Handle different GitHub fetch operations if (toolCall.args.action === 'fetch' || toolCall.args.action === 'list') { if (args) { toolCall.args.repo = args; } } else if (toolCall.args.action === 'execute') { const parts = args.split(/\s+/); if (parts.length >= 1) { toolCall.args.todoId = parts[0]; } if (parts.includes('--live') || parts.includes('--execute')) { toolCall.args.dryRun = false; } } break; case 'gdrive.fetch': // Handle Google Drive fetch operations if (toolCall.args.action === 'fetch') { if (args) { const parts = args.split(/\s+/); if (parts[0]) { toolCall.args.query = parts[0]; } if (parts[1]) { toolCall.args.format = parts[1]; } } } else if (toolCall.args.action === 'execute') { const parts = args.split(/\s+/); if (parts.length >= 1) { toolCall.args.todoId = parts[0]; } if (parts.includes('--live') || parts.includes('--execute')) { toolCall.args.dryRun = false; } } break; case 'gh.issues.list': const ghParts = args.split('/'); if (ghParts.length >= 2) { toolCall.args.owner = ghParts[0]; toolCall.args.repo = ghParts[1]; } break; default: // Generic argument passing // Special parsing for cream.fetch and rss.read if (toolCall.tool === "cream.fetch") { const n = parseInt(args, 10); if (!isNaN(n) && n > 0) toolCall.args.limit = n; } else if (toolCall.tool === "rss.read") { const n = parseInt(args, 10); if (!isNaN(n) && n > 0) toolCall.args.rss_id = n; else if (args) toolCall.args.rss_id = args; } else { toolCall.args.input = args; } break; } } /** * Get help text for available sigils */ getHelp() { const categories = { 'Math & Calculations': ['@calc', '@calculate', '@math'], 'Data Analysis': ['@analyze', '@data', '@csv'], 'File Operations': ['@file', '@read', '@write', '@count', '@upper', '@lower'], 'System Info': ['@system', '@info', '@status'], 'Communication': ['@email', '@post', '@share', '@publish', '@whatsapp', '@sms'], 'Web & Search': ['@search', '@google'], 'Development': ['@compile', '@tex', '@latex', '@pdf'], 'Media Processing': ['@image', '@video', '@convert', '@resize'], 'GitHub': ['@github', '@issue'], 'Tasks': ['@todo', '@task', '@run', '@shell'] }; let help = '🎯 **Available C9AI Sigils:**\\n\\n'; for (const [category, sigils] of Object.entries(categories)) { help += `**${category}:**\\n`; help += sigils.map(s => ` ${s}`).join('\\n') + '\\n\\n'; } help += '💡 **Usage Examples:**\\n'; help += '• `@calc 22/7` - Calculate mathematical expressions\\n'; help += '• `@analyze data.csv` - Analyze CSV files\\n'; help += '• `@count document.txt` - Count lines in files\\n'; help += '• `@search quantum computing` - Web search\\n'; help += '• `@email user@example.com "Subject" "Message"` - Send emails\\n'; help += '• `@post "Content here" visibility:public` - Create posts on Cream\\n'; help += '• `@compile paper.tex` - Compile LaTeX documents\\n'; return help; } } module.exports = SigilRouter;