UNPKG

@ace-sdk/cli

Version:

ACE CLI - Command-line tool for intelligent pattern learning and playbook management

313 lines • 14.1 kB
/** * Learning command - submit execution traces to update playbook * * Supports SSE streaming for real-time progress feedback (v2.1.0+) * Supports git context for AI-Trail pattern correlation (v2.2.0+) * * @see https://github.com/ce-dot-net/ace-sdk/issues/16 */ import { readFileSync } from 'fs'; import { globalOptions } from '../cli.js'; import { createContext, loadConfig } from '../types/config.js'; import { ACEServerClient } from '../services/server-client.js'; import { LEARNING_STAGE_ICONS } from '../types/pattern.js'; import { Logger } from '../services/logger.js'; import { GitContextService } from '../services/git-context.js'; import chalk from 'chalk'; /** * Submit learning event to update playbook */ export async function learnCommand(options) { const logger = new Logger(globalOptions); let trace = null; // Read from file if specified if (options.transcript) { try { const content = readFileSync(options.transcript, 'utf8'); trace = JSON.parse(content); } catch (error) { if (logger.isJson()) { logger.error('Failed to read transcript file'); } else { logger.error(chalk.red(`Error: Failed to read transcript file: ${error instanceof Error ? error.message : String(error)}`)); } process.exit(1); } } // Read from stdin if specified else if (options.stdin) { try { const stdinData = readFileSync(0, 'utf8'); // fd 0 is stdin trace = JSON.parse(stdinData.trim()); } catch (error) { if (logger.isJson()) { logger.error('Failed to read from stdin'); } else { logger.error(chalk.red(`Error: Failed to read from stdin: ${error instanceof Error ? error.message : String(error)}`)); } process.exit(1); } } // Build from command-line options else if (options.task && (options.success || options.failure) && options.output) { const trajectory = [{ step: 1, action: options.task, args: {}, result: 'Completed' }]; trace = { task: options.task, trajectory, result: { success: options.success || false, output: options.output, error: options.failure ? 'Task failed' : undefined }, playbook_used: [], timestamp: new Date().toISOString() }; // Add git context if enabled (default: true) if (options.gitContext !== false) { const gitContext = detectGitContextForTrace(options, logger); if (gitContext) { trace.git = gitContext; logger.debug(`Git context detected: ${gitContext.commit_hash.substring(0, 7)} on ${gitContext.branch}`); } } } else { if (logger.isJson()) { logger.error('Must provide either --transcript, --stdin, or --task + --success/--failure + --output'); } else { logger.error(chalk.red('Error: Must provide either:')); logger.info(chalk.dim(' - --transcript <file>')); logger.info(chalk.dim(' - --stdin')); logger.info(chalk.dim(' - --task + --success/--failure + --output')); } process.exit(1); } if (!trace) { if (logger.isJson()) { logger.error('Failed to create execution trace'); } else { logger.error(chalk.red('Error: Failed to create execution trace')); } process.exit(1); } // Use streaming by default (options.stream is true unless --no-stream) const useStream = options.stream !== false; // Verbosity priority: CLI flag > ENV > config file > default // Note: ENV is already loaded into config by loadConfig(), but CLI --verbosity has highest priority const config = loadConfig(); const envVerbosity = process.env.ACE_VERBOSITY?.toLowerCase(); const verbosity = options.verbosity || (envVerbosity === 'compact' || envVerbosity === 'detailed' ? envVerbosity : undefined) || config.verbosity || 'compact'; // Start with spinner for initial message let spinner = logger.spinner('Submitting learning event...'); try { const context = await createContext({ org: globalOptions.org, project: globalOptions.project }); const client = new ACEServerClient(context, logger); // Check server config for learning settings try { const serverConfig = await client.getConfig(); const runtimeSettings = serverConfig?.runtime_settings || {}; // Warn if learning is disabled on server if (runtimeSettings.learningEnabled === false) { logger.warn('Learning is disabled on the server. Trace will be stored but not processed.'); } // Log server thresholds logger.debug(`Learning settings: enabled=${runtimeSettings.learningEnabled ?? true}, min_confidence=${runtimeSettings.learningMinConfidence ?? 0.3}, min_tokens=${runtimeSettings.learningMinTokens ?? 100}`); } catch (error) { logger.debug('Failed to fetch server config, proceeding with submission'); } let result; if (useStream) { // Use SSE streaming for real-time progress logger.debug('Using SSE streaming for learning submission'); result = await client.storeExecutionTraceStream(trace, { verbosity, onEvent: (event) => { // Update spinner with progress const icon = LEARNING_STAGE_ICONS[event.stage] || 'ā³'; if (event.stage === 'error') { spinner?.fail(`${icon} ${event.message}`); return; } if (event.stage === 'done') { // Don't update spinner here - we'll handle it after return; } // Update spinner text with stage progress if (spinner) { spinner.text = `${icon} ${event.message}`; } // In detailed mode, show extra data if (verbosity === 'detailed' && event.data && event.stage === 'synthesizing') { const insightsCount = event.data.insights_count; if (insightsCount !== undefined) { logger.debug(` Extracted ${insightsCount} insights`); } } }, onError: (error) => { logger.debug(`SSE stream error: ${error.message}`); }, fallbackOnError: true }); } else { // Use traditional sync POST logger.debug('Using sync POST for learning submission (streaming disabled)'); result = await client.storeExecutionTrace(trace); } // Invalidate cache so next request gets fresh data client.invalidateCache(); spinner?.succeed('Learning event submitted'); if (logger.isJson()) { // JSON output includes full response logger.output({ success: true, message: 'Execution trace stored successfully', task: trace.task, timestamp: trace.timestamp, analysis_performed: result.analysis_performed, learning_statistics: result.learning_statistics }); } else { // Human-readable output logger.info(chalk.green('\nāœ… Learning event submitted successfully')); logger.info(chalk.dim(` Task: ${trace.task}`)); logger.info(chalk.dim(` Timestamp: ${trace.timestamp}`)); // Show learning statistics if available (server v3.10.0+) if (result.learning_statistics) { const stats = result.learning_statistics; logger.info(chalk.bold('\nšŸ“Š Learning Results:')); // Show creation/update counts if (stats.patterns_created > 0) { logger.info(chalk.green(` • ${stats.patterns_created} new pattern${stats.patterns_created === 1 ? '' : 's'} created`)); } if (stats.patterns_updated > 0) { logger.info(chalk.cyan(` • ${stats.patterns_updated} pattern${stats.patterns_updated === 1 ? '' : 's'} updated`)); } if (stats.patterns_pruned > 0) { logger.info(chalk.yellow(` • ${stats.patterns_pruned} low-quality pattern${stats.patterns_pruned === 1 ? '' : 's'} pruned`)); } if (stats.patterns_deduplicated > 0) { logger.info(chalk.blue(` • ${stats.patterns_deduplicated} duplicate${stats.patterns_deduplicated === 1 ? '' : 's'} merged`)); } // Show section breakdown if verbose and available if (logger.isVerbose() && stats.by_section) { const sections = stats.by_section; const totalSectionChanges = (sections.strategies_and_hard_rules || 0) + (sections.useful_code_snippets || 0) + (sections.troubleshooting_and_pitfalls || 0) + (sections.apis_to_use || 0); if (totalSectionChanges > 0) { logger.info(chalk.bold('\n Affected Sections:')); if (sections.strategies_and_hard_rules > 0) { logger.info(chalk.dim(` - Strategies & Hard Rules: ${sections.strategies_and_hard_rules}`)); } if (sections.useful_code_snippets > 0) { logger.info(chalk.dim(` - Useful Code Snippets: ${sections.useful_code_snippets}`)); } if (sections.troubleshooting_and_pitfalls > 0) { logger.info(chalk.dim(` - Troubleshooting & Pitfalls: ${sections.troubleshooting_and_pitfalls}`)); } if (sections.apis_to_use > 0) { logger.info(chalk.dim(` - APIs to Use: ${sections.apis_to_use}`)); } } } // Show quality metrics if available logger.info(chalk.bold('\n Quality Metrics:')); if (stats.average_confidence !== undefined) { logger.info(chalk.dim(` - Average confidence: ${(stats.average_confidence * 100).toFixed(0)}%`)); } if (stats.helpful_delta !== undefined && stats.helpful_delta > 0) { logger.info(chalk.dim(` - Helpful feedback: +${stats.helpful_delta}`)); } if (stats.analysis_time_seconds !== undefined) { logger.info(chalk.dim(` - Analysis time: ${stats.analysis_time_seconds.toFixed(1)}s`)); } logger.info(''); } else { // Fallback for older servers without statistics logger.info(chalk.dim(' Server is analyzing and updating playbook...\n')); } } } catch (error) { spinner?.fail('Failed to submit learning event'); if (logger.isJson()) { logger.error(error instanceof Error ? error.message : String(error)); } else { logger.error(chalk.red(`\nError: ${error instanceof Error ? error.message : String(error)}\n`)); } process.exit(1); } } /** * Detect git context for execution trace. * * Uses explicit options if provided, otherwise auto-detects from cwd. * Returns null gracefully if not in a git repository. * * @since v2.2.0 */ function detectGitContextForTrace(options, logger) { try { const gitService = new GitContextService(); // Check if we're in a git repo if (!gitService.isGitRepository()) { logger.debug('Not in a git repository, skipping git context'); return null; } // Use explicit commit if provided const commitHash = options.gitCommit || gitService.getCurrentHead(); if (!commitHash) { logger.debug('No commit found, skipping git context'); return null; } // Use explicit branch if provided, otherwise detect const branch = options.gitBranch || gitService.getBranch(); if (!branch) { logger.debug('No branch found, skipping git context'); return null; } // Build context const context = { commit_hash: commitHash, branch, files_changed: gitService.getChangedFiles(commitHash), commit_message: gitService.getCommitMessage(commitHash) || undefined, timestamp: gitService.getCommitTimestamp(commitHash) || undefined, author: gitService.getCommitAuthor(commitHash) || undefined, parent_commits: gitService.getParentCommits(commitHash) }; // Get stats (may fail for initial commit) const stats = gitService.getCommitStats(commitHash); if (stats) { context.insertions = stats.insertions; context.deletions = stats.deletions; } return context; } catch (error) { logger.debug(`Failed to detect git context: ${error instanceof Error ? error.message : String(error)}`); return null; } } //# sourceMappingURL=learn.js.map