c9ai
Version:
Universal AI assistant with vibe-based workflows, hybrid cloud+local AI, and comprehensive tool integration
489 lines (415 loc) • 13.9 kB
JavaScript
;
/**
* CloudAIBridge - Parses cloud AI responses for executable C9AI actions
* and manages the handshake between cloud intelligence and local execution
*/
const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");
class CloudAIBridge {
constructor() {
this.actionQueue = [];
this.executionHistory = [];
this.supportedSigils = new Set([
'calc', 'analyze', 'system', 'count', 'read', 'write', 'email',
'search', 'github-fetch', 'github-list', 'github-run',
'gdrive-fetch', 'gdrive-list', 'gdrive-run', 'file', 'process',
'todo', 'task', 'run', 'shell', 'whatsapp', 'sms', 'compile',
'tex', 'latex', 'pdf', 'image', 'video', 'convert', 'resize'
]);
}
/**
* Parse cloud AI response for executable actions
*/
parseCloudResponse(response) {
try {
// Handle both string responses and objects
const responseText = typeof response === 'string' ? response :
(response.text || response.message || JSON.stringify(response));
// Look for JSON blocks in various formats
const jsonPatterns = [
/```json\s*(\{[\s\S]*?\})\s*```/gi,
/```\s*(\{[\s\S]*?\})\s*```/gi,
/(\{[\s\S]*?"actions"[\s\S]*?\})/gi,
/(\{[\s\S]*?"message"[\s\S]*?"actions"[\s\S]*?\})/gi
];
for (const pattern of jsonPatterns) {
const matches = Array.from(responseText.matchAll(pattern));
for (const match of matches) {
try {
const jsonStr = match[1];
const parsed = JSON.parse(jsonStr);
// Validate it's a C9AI action format
if (this.isValidActionFormat(parsed)) {
return {
success: true,
originalResponse: responseText,
parsedActions: parsed,
extractedFrom: 'json_block'
};
}
} catch (e) {
continue; // Try next match
}
}
}
// Look for inline sigils in the response text
const inlineActions = this.extractInlineSigils(responseText);
if (inlineActions.length > 0) {
return {
success: true,
originalResponse: responseText,
parsedActions: {
message: responseText,
actions: inlineActions,
execution_mode: 'sequential',
requires_confirmation: true
},
extractedFrom: 'inline_sigils'
};
}
return {
success: false,
originalResponse: responseText,
error: 'No executable actions found in response'
};
} catch (error) {
return {
success: false,
error: `Failed to parse response: ${error.message}`,
originalResponse: response
};
}
}
/**
* Check if parsed JSON is valid C9AI action format
*/
isValidActionFormat(obj) {
if (!obj || typeof obj !== 'object') return false;
// Must have actions array
if (!Array.isArray(obj.actions)) return false;
// Validate each action
for (const action of obj.actions) {
if (!action.sigil || !action.sigil.startsWith('@')) return false;
if (!action.id || !action.description) return false;
const sigilName = action.sigil.slice(1); // Remove @
if (!this.supportedSigils.has(sigilName)) return false;
}
return true;
}
/**
* Extract inline sigils from response text
*/
extractInlineSigils(text) {
const actions = [];
const sigilPattern = /@(\w+(?:-\w+)*)\s+([^\n\r]*)/gi;
let match;
let actionId = 1;
while ((match = sigilPattern.exec(text)) !== null) {
const sigilName = match[1];
const args = match[2].trim();
if (this.supportedSigils.has(sigilName)) {
actions.push({
id: `inline-${actionId++}`,
sigil: `@${sigilName}`,
args: args,
description: `Execute ${sigilName} command`,
risk_level: this.assessSigilRisk(sigilName),
estimated_time: this.estimateSigilTime(sigilName),
source: 'inline_extraction'
});
}
}
return actions;
}
/**
* Prepare actions for execution - validation and planning
*/
async prepareActions(actions) {
const validationResults = [];
const dependencies = [];
let totalEstimatedTime = 0;
for (const action of actions) {
// Validate action
const validation = await this.validateAction(action);
validationResults.push(validation);
// Check dependencies
const deps = this.findActionDependencies(action, actions);
if (deps.length > 0) {
dependencies.push({ actionId: action.id, dependsOn: deps });
}
// Add to estimated time
totalEstimatedTime += this.parseTime(action.estimated_time || '10s');
}
// Build execution plan
const executionPlan = {
totalActions: actions.length,
validActions: validationResults.filter(v => v.valid).length,
totalEstimatedTime: `${totalEstimatedTime}s`,
executionOrder: this.planExecutionOrder(actions, dependencies),
riskAssessment: this.assessOverallRisk(actions),
dependencies: dependencies,
validationResults: validationResults
};
return executionPlan;
}
/**
* Validate individual action
*/
async validateAction(action) {
const result = {
actionId: action.id,
valid: true,
warnings: [],
errors: [],
requiredFiles: [],
permissions: []
};
try {
// Check sigil exists
const sigilName = action.sigil.slice(1);
if (!this.supportedSigils.has(sigilName)) {
result.valid = false;
result.errors.push(`Unknown sigil: ${action.sigil}`);
return result;
}
// Validate arguments based on sigil
const argValidation = this.validateSigilArgs(sigilName, action.args);
if (!argValidation.valid) {
result.valid = false;
result.errors.push(...argValidation.errors);
}
result.warnings.push(...argValidation.warnings);
// Check file permissions for file operations
if (['read', 'write', 'analyze', 'count', 'file', 'process'].includes(sigilName)) {
const filePath = this.extractFilePath(action.args);
if (filePath) {
result.requiredFiles.push(filePath);
if (sigilName === 'read' || sigilName === 'analyze' || sigilName === 'count') {
if (!fs.existsSync(filePath)) {
result.warnings.push(`File may not exist: ${filePath}`);
}
}
if (sigilName === 'write') {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
result.warnings.push(`Directory may not exist: ${dir}`);
}
}
}
}
// Check network permissions for external operations
if (['email', 'search', 'github-fetch', 'gdrive-fetch'].includes(sigilName)) {
result.permissions.push('network_access');
}
// Check system permissions for system operations
if (['system', 'shell', 'run'].includes(sigilName)) {
result.permissions.push('system_access');
}
} catch (error) {
result.valid = false;
result.errors.push(`Validation failed: ${error.message}`);
}
return result;
}
/**
* Validate sigil-specific arguments
*/
validateSigilArgs(sigil, args) {
const result = { valid: true, errors: [], warnings: [] };
try {
switch (sigil) {
case 'calc':
if (!args || args.trim() === '') {
result.valid = false;
result.errors.push('Calculator requires an expression');
}
break;
case 'read':
case 'analyze':
case 'count':
if (!args || args.trim() === '') {
result.valid = false;
result.errors.push(`${sigil} requires a file path`);
}
break;
case 'write':
if (!args.includes('->')) {
result.warnings.push('Write command should include -> separator');
}
break;
case 'email':
if (!args.includes('@')) {
result.warnings.push('Email command may be missing recipient');
}
break;
case 'github-fetch':
case 'gdrive-fetch':
// These can work with default arguments
break;
default:
// Generic validation
if (!args) {
result.warnings.push(`${sigil} has no arguments`);
}
}
} catch (error) {
result.errors.push(`Argument validation failed: ${error.message}`);
}
return result;
}
/**
* Find dependencies between actions
*/
findActionDependencies(action, allActions) {
const dependencies = [];
// Look for variable references like ${action-1.result}
const variablePattern = /\$\{([^}]+)\}/g;
let match;
const searchText = action.args || '';
while ((match = variablePattern.exec(searchText)) !== null) {
const varRef = match[1];
const actionRef = varRef.split('.')[0];
// Find the referenced action
const depAction = allActions.find(a => a.id === actionRef);
if (depAction) {
dependencies.push(actionRef);
}
}
return dependencies;
}
/**
* Plan optimal execution order considering dependencies
*/
planExecutionOrder(actions, dependencies) {
const order = [];
const completed = new Set();
const remaining = [...actions];
// Simple topological sort
while (remaining.length > 0) {
let progress = false;
for (let i = remaining.length - 1; i >= 0; i--) {
const action = remaining[i];
const actionDeps = dependencies.find(d => d.actionId === action.id);
// Check if all dependencies are completed
if (!actionDeps || actionDeps.dependsOn.every(dep => completed.has(dep))) {
order.push({
position: order.length + 1,
actionId: action.id,
sigil: action.sigil,
description: action.description
});
completed.add(action.id);
remaining.splice(i, 1);
progress = true;
}
}
// Break circular dependencies
if (!progress && remaining.length > 0) {
const action = remaining.shift();
order.push({
position: order.length + 1,
actionId: action.id,
sigil: action.sigil,
description: action.description,
warning: 'Circular dependency detected'
});
completed.add(action.id);
}
}
return order;
}
/**
* Assess overall risk level for action set
*/
assessOverallRisk(actions) {
const risks = actions.map(a => a.risk_level || this.assessSigilRisk(a.sigil.slice(1)));
if (risks.includes('high')) return 'high';
if (risks.includes('medium')) return 'medium';
return 'low';
}
/**
* Assess risk level for specific sigil
*/
assessSigilRisk(sigil) {
const highRisk = ['shell', 'run', 'email', 'system'];
const mediumRisk = ['write', 'github-fetch', 'gdrive-fetch'];
if (highRisk.includes(sigil)) return 'high';
if (mediumRisk.includes(sigil)) return 'medium';
return 'low';
}
/**
* Estimate execution time for sigil
*/
estimateSigilTime(sigil) {
const timeEstimates = {
'calc': '5s',
'read': '10s',
'write': '10s',
'analyze': '30s',
'system': '5s',
'count': '15s',
'search': '20s',
'email': '10s',
'github-fetch': '30s',
'gdrive-fetch': '45s'
};
return timeEstimates[sigil] || '10s';
}
/**
* Parse time string to seconds
*/
parseTime(timeStr) {
const match = timeStr.match(/(\d+)([sm]?)/);
if (!match) return 10;
const value = parseInt(match[1]);
const unit = match[2] || 's';
return unit === 'm' ? value * 60 : value;
}
/**
* Extract file path from arguments
*/
extractFilePath(args) {
if (!args) return null;
// Handle various formats
if (args.includes('->')) {
return args.split('->')[0].trim();
}
// First word is likely the file path
return args.split(/\s+/)[0];
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `session_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
}
/**
* Save execution history
*/
saveExecutionHistory(sessionId, actions, results) {
try {
const historyEntry = {
sessionId,
timestamp: new Date().toISOString(),
actions: actions,
results: results,
summary: {
totalActions: actions.length,
successfulActions: results.filter(r => r.success).length,
failedActions: results.filter(r => !r.success).length
}
};
this.executionHistory.push(historyEntry);
// Keep only last 100 sessions
if (this.executionHistory.length > 100) {
this.executionHistory = this.executionHistory.slice(-100);
}
return historyEntry;
} catch (error) {
console.warn('Failed to save execution history:', error.message);
return null;
}
}
}
module.exports = CloudAIBridge;