From f4a0e49002efeeefdd8e5e4f56793d8bbee88b7e Mon Sep 17 00:00:00 2001 From: Gemini AI Date: Sun, 14 Dec 2025 13:37:06 +0400 Subject: [PATCH] Smart Repair v2.0: AI-powered with 3 Qwen models, offline fallback, OAuth trigger --- bin/smart-repair.mjs | 444 +++++++++++++++++++++++++++++-------------- 1 file changed, 297 insertions(+), 147 deletions(-) diff --git a/bin/smart-repair.mjs b/bin/smart-repair.mjs index bc0b691..0dcf683 100644 --- a/bin/smart-repair.mjs +++ b/bin/smart-repair.mjs @@ -1,22 +1,51 @@ #!/usr/bin/env node /** - * OpenQode Smart Repair Agent - * A specialized TUI for diagnosing and fixing bugs in the main TUI IDE + * OpenQode Smart Repair Agent v2.0 + * AI-Powered TUI Self-Healing System * - * This agent has ONE mission: Repair the TUI when it crashes + * Features: + * - Qwen AI integration for intelligent error analysis + * - 3 model choices (Qwen Coder Plus, Qwen Plus, Qwen Turbo) + * - Offline fallback for common issues + * - OAuth trigger when auth is missing/expired */ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const fs = require('fs'); const path = require('path'); -const { execSync, spawnSync } = require('child_process'); +const { execSync, spawn } = require('child_process'); const readline = require('readline'); // File paths relative to package root const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..'); const MAIN_TUI = path.join(ROOT, 'bin', 'opencode-ink.mjs'); const PACKAGE_JSON = path.join(ROOT, 'package.json'); +const TOKENS_FILE = path.join(ROOT, 'tokens.json'); + +// API Configuration +const DASHSCOPE_API = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'; + +// Model catalog with 3 Qwen options +const MODELS = { + '1': { + id: 'qwen-coder-plus', + name: 'Qwen Coder Plus', + description: 'Best for code analysis and bug fixing' + }, + '2': { + id: 'qwen-plus', + name: 'Qwen Plus', + description: 'General purpose, balanced' + }, + '3': { + id: 'qwen-turbo', + name: 'Qwen Turbo', + description: 'Fast responses, simpler issues' + } +}; + +let selectedModel = MODELS['1']; // Default: Qwen Coder Plus // Colors for terminal output const C = { @@ -27,185 +56,289 @@ const C = { red: '\x1b[31m', magenta: '\x1b[35m', bold: '\x1b[1m', - dim: '\x1b[2m' + dim: '\x1b[2m', + white: '\x1b[37m' }; const banner = () => { console.clear(); console.log(C.magenta + C.bold); console.log(' ╔═══════════════════════════════════════════╗'); - console.log(' ║ 🔧 OpenQode Smart Repair Agent 🔧 ║'); - console.log(' ║ TUI Self-Healing System ║'); + console.log(' ║ 🔧 OpenQode Smart Repair Agent v2.0 🔧 ║'); + console.log(' ║ AI-Powered TUI Self-Healing ║'); console.log(' ╚═══════════════════════════════════════════╝'); console.log(C.reset); console.log(C.dim + ' This agent can ONLY repair the TUI. No other tasks.' + C.reset); + console.log(C.cyan + ` Model: ${selectedModel.name}` + C.reset); console.log(''); }; -// Parse error from user input -const parseError = (errorText) => { - const issues = []; - - // Extract file and line number - const lineMatch = errorText.match(/opencode-ink\.mjs:(\d+)/); - const line = lineMatch ? parseInt(lineMatch[1]) : null; - - // Common error patterns - if (errorText.includes('Cannot read properties of null')) { - issues.push({ - type: 'NULL_REFERENCE', - desc: 'Null reference error - attempting to access property on null object', - line - }); - } - if (errorText.includes('Cannot read properties of undefined')) { - issues.push({ - type: 'UNDEFINED_REFERENCE', - desc: 'Undefined reference - variable or property does not exist', - line - }); - } - if (errorText.includes('useMemo') || errorText.includes('useEffect') || errorText.includes('useState')) { - issues.push({ - type: 'REACT_HOOKS', - desc: 'React hooks error - possible multiple React versions or hooks called outside component', - line - }); - } - if (errorText.includes('ink-syntax-highlight')) { - issues.push({ - type: 'INK_SYNTAX_HIGHLIGHT', - desc: 'Extraneous ink-syntax-highlight package causing React conflict', - line - }); - } - if (errorText.includes('ENOENT')) { - issues.push({ - type: 'FILE_NOT_FOUND', - desc: 'File or directory not found', - line - }); - } - if (errorText.includes('SyntaxError')) { - issues.push({ - type: 'SYNTAX_ERROR', - desc: 'JavaScript syntax error in code', - line - }); - } - - return issues; +// Get Qwen auth token +const getAuthToken = () => { + try { + if (fs.existsSync(TOKENS_FILE)) { + const tokens = JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf8')); + return tokens.access_token || null; + } + // Try qwen CLI config + const configPath = path.join(process.env.HOME || process.env.USERPROFILE, '.qwen', 'config.json'); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + return config.access_token || config.api_key || null; + } + } catch (e) { /* ignore */ } + return null; }; -// Automatic repair functions -const repairs = { - REACT_HOOKS: () => { - console.log(C.yellow + '[*] Fixing React hooks conflict...' + C.reset); - // Delete node_modules and reinstall - const nmPath = path.join(ROOT, 'node_modules'); - const lockPath = path.join(ROOT, 'package-lock.json'); - if (fs.existsSync(nmPath)) { - console.log(' Removing node_modules...'); - fs.rmSync(nmPath, { recursive: true, force: true }); +// Trigger OAuth authentication +const triggerOAuth = async () => { + console.log(C.yellow + '\n[!] Authentication required. Opening browser for Qwen OAuth...' + C.reset); + try { + const { QwenOAuth } = await import('../qwen-oauth.mjs'); + const oauth = new QwenOAuth(); + const tokens = await oauth.authenticate(); + if (tokens && tokens.access_token) { + fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2)); + console.log(C.green + '[✓] Authentication successful!' + C.reset); + return tokens.access_token; } - if (fs.existsSync(lockPath)) { - console.log(' Removing package-lock.json...'); - fs.unlinkSync(lockPath); - } - console.log(' Running npm install...'); - try { - execSync('npm install --legacy-peer-deps', { cwd: ROOT, stdio: 'inherit' }); - return { success: true, msg: 'Dependencies reinstalled with React overrides' }; - } catch (e) { - return { success: false, msg: 'npm install failed: ' + e.message }; - } - }, + } catch (e) { + console.log(C.red + `[✗] OAuth failed: ${e.message}` + C.reset); + } + return null; +}; - INK_SYNTAX_HIGHLIGHT: () => { - console.log(C.yellow + '[*] Removing extraneous ink-syntax-highlight...' + C.reset); - const ish = path.join(ROOT, 'node_modules', 'ink-syntax-highlight'); - if (fs.existsSync(ish)) { - fs.rmSync(ish, { recursive: true, force: true }); - return { success: true, msg: 'Removed ink-syntax-highlight package' }; - } - // If not found, do full reinstall - return repairs.REACT_HOOKS(); - }, +// Call Qwen AI API +const callQwenAI = async (prompt, onChunk = null) => { + let token = getAuthToken(); - NULL_REFERENCE: (issue) => { - if (!issue.line) { - return { success: false, msg: 'Could not determine line number for null reference fix' }; + if (!token) { + token = await triggerOAuth(); + if (!token) { + return { success: false, error: 'No auth token available', response: '' }; } - console.log(C.yellow + `[*] Checking line ${issue.line} for null safety...` + C.reset); - // Read the file and show the problematic line - const code = fs.readFileSync(MAIN_TUI, 'utf8'); - const lines = code.split('\n'); - const problemLine = lines[issue.line - 1]; - console.log(C.dim + ` Line ${issue.line}: ${problemLine.trim().substring(0, 60)}...` + C.reset); - return { success: false, msg: 'Manual review needed. Please report to developer with error details.' }; - }, + } - UNDEFINED_REFERENCE: (issue) => repairs.NULL_REFERENCE(issue), + try { + const response = await fetch(DASHSCOPE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + model: selectedModel.id, + messages: [ + { + role: 'system', + content: `You are the OpenQode Smart Repair Agent. Your ONLY purpose is to diagnose and fix bugs in the OpenQode TUI (Terminal User Interface). - FILE_NOT_FOUND: () => { - console.log(C.yellow + '[*] Checking file structure...' + C.reset); - if (!fs.existsSync(MAIN_TUI)) { - return { success: false, msg: 'Main TUI file missing! Please re-clone the repository.' }; +The TUI is a Node.js/React Ink application located at: +- Main file: bin/opencode-ink.mjs +- Package: package.json + +When given an error: +1. Analyze the error message and stack trace +2. Identify the root cause +3. Provide a specific fix (code change or shell command) +4. Format fixes clearly with code blocks + +You MUST refuse any request that is not about fixing the TUI.` + }, + { role: 'user', content: prompt } + ], + stream: true, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 401) { + // Token expired - try re-auth + console.log(C.yellow + '[!] Token expired, re-authenticating...' + C.reset); + const newToken = await triggerOAuth(); + if (newToken) { + return callQwenAI(prompt, onChunk); // Retry with new token + } + } + return { success: false, error: `API error ${response.status}: ${errorText}`, response: '' }; } - return { success: true, msg: 'File structure appears intact' }; - }, - SYNTAX_ERROR: () => { - console.log(C.yellow + '[*] Checking syntax...' + C.reset); - try { - execSync(`node -c "${MAIN_TUI}"`, { cwd: ROOT }); - return { success: true, msg: 'Syntax check passed' }; - } catch (e) { - return { success: false, msg: 'Syntax error detected. Please pull latest code: git pull origin main' }; + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let fullResponse = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6).trim(); + if (data === '[DONE]') continue; + + try { + const parsed = JSON.parse(data); + const content = parsed.choices?.[0]?.delta?.content || ''; + if (content) { + fullResponse += content; + if (onChunk) onChunk(content); + } + } catch (e) { /* ignore parse errors */ } + } + } } + + return { success: true, response: fullResponse }; + } catch (error) { + return { success: false, error: error.message || 'Network error', response: '' }; + } +}; + +// Offline fallback - common issues detection +const offlineDiagnose = (errorText) => { + const fixes = []; + + // React hooks conflict + if (errorText.includes('useMemo') || errorText.includes('useEffect') || errorText.includes('ink-syntax-highlight')) { + fixes.push({ + issue: 'React version conflict (multiple React instances)', + fix: 'Reinstall dependencies with React overrides', + command: 'rm -rf node_modules package-lock.json && npm install --legacy-peer-deps' + }); + } + + // Null reference errors + if (errorText.includes('Cannot read properties of null') || errorText.includes('Cannot read properties of undefined')) { + const lineMatch = errorText.match(/opencode-ink\.mjs:(\d+)/); + fixes.push({ + issue: 'Null reference error in code', + fix: lineMatch ? `Check line ${lineMatch[1]} for missing null checks` : 'Code needs null safety checks', + command: 'git pull origin main # Get latest bug fixes' + }); + } + + // Module not found + if (errorText.includes('Cannot find module') || errorText.includes('ENOENT')) { + fixes.push({ + issue: 'Missing dependency or file', + fix: 'Reinstall dependencies', + command: 'npm install --legacy-peer-deps' + }); + } + + // Auth errors + if (errorText.includes('401') || errorText.includes('unauthorized') || errorText.includes('auth')) { + fixes.push({ + issue: 'Authentication expired or missing', + fix: 'Trigger OAuth re-authentication', + command: '__OAUTH__' // Special marker for OAuth trigger + }); + } + + // Syntax errors + if (errorText.includes('SyntaxError') || errorText.includes('Unexpected token')) { + fixes.push({ + issue: 'JavaScript syntax error', + fix: 'Pull latest code to get fixes', + command: 'git pull origin main' + }); + } + + return fixes; +}; + +// Execute a fix command +const executeCommand = (command) => { + if (command === '__OAUTH__') { + return triggerOAuth(); + } + + console.log(C.cyan + `\n Executing: ${command}` + C.reset); + try { + execSync(command, { cwd: ROOT, stdio: 'inherit' }); + return true; + } catch (e) { + console.log(C.red + ` Command failed: ${e.message}` + C.reset); + return false; + } +}; + +// Model selection menu +const selectModel = async (rl) => { + console.log(C.cyan + '\n Select AI Model:' + C.reset); + for (const [key, model] of Object.entries(MODELS)) { + const marker = model.id === selectedModel.id ? C.green + ' ←' + C.reset : ''; + console.log(` [${key}] ${model.name} - ${model.description}${marker}`); + } + const choice = await new Promise(r => rl.question(C.magenta + '\n Enter choice (1-3): ' + C.reset, r)); + if (MODELS[choice]) { + selectedModel = MODELS[choice]; + console.log(C.green + `\n [✓] Selected: ${selectedModel.name}` + C.reset); } }; // Main repair function const attemptRepair = async (errorText) => { - console.log(C.cyan + '\n[ANALYZING ERROR...]' + C.reset); - const issues = parseError(errorText); + console.log(C.cyan + '\n[1/3] OFFLINE ANALYSIS...' + C.reset); + const offlineFixes = offlineDiagnose(errorText); - if (issues.length === 0) { - console.log(C.yellow + '[!] Could not identify specific issue from error.' + C.reset); - console.log(' Generic repair: Reinstalling dependencies...'); - const result = repairs.REACT_HOOKS(); - return result; - } + if (offlineFixes.length > 0) { + console.log(C.green + ` Found ${offlineFixes.length} known issue(s):` + C.reset); + offlineFixes.forEach((fix, i) => { + console.log(C.yellow + ` ${i + 1}. ${fix.issue}` + C.reset); + console.log(C.dim + ` Fix: ${fix.fix}` + C.reset); + }); - console.log(C.green + `[✓] Identified ${issues.length} issue(s):` + C.reset); - issues.forEach((issue, i) => { - console.log(` ${i + 1}. ${issue.type}: ${issue.desc}`); - if (issue.line) console.log(` At line: ${issue.line}`); - }); - - console.log(C.cyan + '\n[ATTEMPTING REPAIRS...]' + C.reset); - - for (const issue of issues) { - const repairFn = repairs[issue.type]; - if (repairFn) { - const result = repairFn(issue); - if (result.success) { - console.log(C.green + `[✓] ${result.msg}` + C.reset); - } else { - console.log(C.red + `[✗] ${result.msg}` + C.reset); + // Try offline fixes first + console.log(C.cyan + '\n[2/3] APPLYING OFFLINE FIXES...' + C.reset); + for (const fix of offlineFixes) { + const success = await executeCommand(fix.command); + if (success) { + console.log(C.green + ` [✓] Applied: ${fix.fix}` + C.reset); } } + } else { + console.log(C.yellow + ' No known patterns. Consulting AI...' + C.reset); } - // Verify the fix - console.log(C.cyan + '\n[VERIFYING FIX...]' + C.reset); + // Consult AI for deeper analysis + console.log(C.cyan + '\n[3/3] AI ANALYSIS...' + C.reset); + console.log(C.dim + ` Using ${selectedModel.name}...` + C.reset); + + const prompt = `Analyze this OpenQode TUI error and provide a fix: + +\`\`\` +${errorText} +\`\`\` + +The TUI is a React Ink app at bin/opencode-ink.mjs. Provide: +1. Root cause analysis +2. Specific fix (code change or command) +3. Prevention tips`; + + process.stdout.write(C.white + '\n '); + const result = await callQwenAI(prompt, (chunk) => { + process.stdout.write(chunk); + }); + console.log(C.reset); + + if (!result.success) { + console.log(C.yellow + `\n [!] AI unavailable: ${result.error}` + C.reset); + console.log(C.dim + ' Offline fixes have been applied if any were found.' + C.reset); + } + + // Verify fix + console.log(C.cyan + '\n[VERIFYING...]' + C.reset); try { execSync(`node -c "${MAIN_TUI}"`, { cwd: ROOT }); console.log(C.green + '[✓] Syntax check passed!' + C.reset); return { success: true }; } catch (e) { - console.log(C.red + '[✗] Still has issues.' + C.reset); + console.log(C.yellow + '[!] Syntax issues remain. Try relaunching or paste the new error.' + C.reset); return { success: false }; } }; @@ -221,20 +354,37 @@ const runInteractive = async () => { while (true) { banner(); - console.log(C.yellow + ' Paste the error message you received from TUI crash.' + C.reset); - console.log(C.dim + ' (Type "quit" to exit, or press Ctrl+C)' + C.reset); + console.log(C.yellow + ' Commands: ' + C.reset); + console.log(C.dim + ' • Paste an error message to analyze' + C.reset); + console.log(C.dim + ' • Type "model" to change AI model' + C.reset); + console.log(C.dim + ' • Type "auth" to trigger OAuth' + C.reset); + console.log(C.dim + ' • Type "quit" to exit' + C.reset); console.log(''); const errorText = await question(C.magenta + '> ' + C.reset); + if (!errorText.trim()) continue; + if (errorText.toLowerCase() === 'quit' || errorText.toLowerCase() === 'exit') { console.log(C.cyan + '\nGoodbye! Try launching TUI again.' + C.reset); rl.close(); process.exit(0); } + if (errorText.toLowerCase() === 'model') { + await selectModel(rl); + await question('\nPress Enter to continue...'); + continue; + } + + if (errorText.toLowerCase() === 'auth') { + await triggerOAuth(); + await question('\nPress Enter to continue...'); + continue; + } + // Check if user is asking for something other than repair - const nonRepairKeywords = ['create', 'build', 'write code', 'make a', 'help me', 'how to', 'what is']; + const nonRepairKeywords = ['create', 'build', 'write code', 'make a', 'help me write', 'how to build']; if (nonRepairKeywords.some(kw => errorText.toLowerCase().includes(kw))) { console.log(C.red + '\n[!] I can ONLY repair the TUI. For other tasks, use the main TUI IDE.' + C.reset); await question('\nPress Enter to continue...');