Files
OpenQode/bin/smart-repair.mjs

416 lines
16 KiB
JavaScript

#!/usr/bin/env node
/**
* OpenQode Smart Repair Agent v2.0
* AI-Powered TUI Self-Healing System
*
* 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, 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 = {
reset: '\x1b[0m',
cyan: '\x1b[36m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
magenta: '\x1b[35m',
bold: '\x1b[1m',
dim: '\x1b[2m',
white: '\x1b[37m'
};
const banner = () => {
console.clear();
console.log(C.magenta + C.bold);
console.log(' ╔═══════════════════════════════════════════╗');
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('');
};
// Get Qwen auth token - checks multiple locations
const getAuthToken = () => {
// Multiple paths where tokens might be stored
const tokenPaths = [
path.join(ROOT, '.qwen-tokens.json'), // QwenOAuth default
path.join(ROOT, 'tokens.json'), // Alternative location
path.join(process.env.HOME || process.env.USERPROFILE || '', '.qwen', 'config.json'), // qwen CLI
path.join(process.env.HOME || process.env.USERPROFILE || '', '.qwen', 'tokens.json'),
];
for (const tokenPath of tokenPaths) {
try {
if (fs.existsSync(tokenPath)) {
const tokens = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
const token = tokens.access_token || tokens.api_key || tokens.token;
if (token) {
console.log(C.dim + ` [Found token in ${path.basename(tokenPath)}]` + C.reset);
return token;
}
}
} catch (e) { /* ignore */ }
}
return null;
};
// Trigger OAuth authentication
const triggerOAuth = async () => {
console.log(C.yellow + '\n[!] Authentication required. Starting Qwen OAuth...' + C.reset);
try {
const { QwenOAuth } = await import('../qwen-oauth.mjs');
const oauth = new QwenOAuth();
// Start device code flow
const deviceInfo = await oauth.startDeviceFlow();
console.log('');
console.log(C.magenta + ' ╔═══════════════════════════════════════════╗' + C.reset);
console.log(C.magenta + ' ║ QWEN AUTHENTICATION ║' + C.reset);
console.log(C.magenta + ' ╚═══════════════════════════════════════════╝' + C.reset);
console.log('');
console.log(C.yellow + ' 1. Open this URL in your browser:' + C.reset);
console.log(C.cyan + ` ${deviceInfo.verificationUriComplete || deviceInfo.verificationUri}` + C.reset);
console.log('');
if (deviceInfo.userCode) {
console.log(C.yellow + ' 2. Enter this code if prompted:' + C.reset);
console.log(C.green + C.bold + ` ${deviceInfo.userCode}` + C.reset);
console.log('');
}
console.log(C.dim + ' Waiting for you to complete login in browser...' + C.reset);
// Try to open browser automatically
const { exec } = require('child_process');
const url = deviceInfo.verificationUriComplete || deviceInfo.verificationUri;
const platform = process.platform;
const cmd = platform === 'darwin' ? `open "${url}"` : platform === 'win32' ? `start "" "${url}"` : `xdg-open "${url}"`;
exec(cmd, () => { });
// Poll for tokens
const tokens = await oauth.pollForTokens();
if (tokens && tokens.access_token) {
// Save tokens
oauth.saveTokens(tokens);
fs.writeFileSync(TOKENS_FILE, JSON.stringify({
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: tokens.expires_at || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
}, null, 2));
console.log(C.green + '\n[✓] Authentication successful!' + C.reset);
return tokens.access_token;
}
} catch (e) {
console.log(C.red + `[✗] OAuth failed: ${e.message}` + C.reset);
if (e.message.includes('Client ID')) {
console.log(C.yellow + '\n To fix: Copy config.example.cjs to config.cjs and add your QWEN_OAUTH_CLIENT_ID' + C.reset);
}
}
return null;
};
// Call Qwen AI using same method as TUI (QwenOAuth.sendMessage)
const callQwenAI = async (prompt, onChunk = null) => {
try {
const { QwenOAuth } = await import('../qwen-oauth.mjs');
const oauth = new QwenOAuth();
// System context for repair agent (invisible to output)
const systemContext = `You are a friendly AI repair technician for OpenQode TUI.
Speak naturally and helpfully. Your ONLY job is diagnosing and fixing TUI bugs.
TUI Details:
- Main file: bin/opencode-ink.mjs (React Ink app)
- Package: package.json
When analyzing errors:
1. Explain what went wrong in simple terms
2. Provide the fix (code change or command)
3. Use code blocks for fixes
If the user asks about something unrelated to TUI repair, politely remind them you can only help with TUI issues.
User says: ${prompt}`;
// Call AI silently - no debug output
const result = await oauth.sendMessage(systemContext, selectedModel.id, null, onChunk);
if (result && result.response) {
return { success: true, response: result.response };
} else if (result && result.error) {
return { success: false, error: result.error, response: '' };
} else {
return { success: true, response: result || '' };
}
} catch (error) {
// If qwen CLI not found, give helpful message
if (error.message && error.message.includes('ENOENT')) {
return {
success: false,
error: 'qwen CLI not installed. Install with: npm install -g @qwen-code/qwen-code',
response: ''
};
}
return { success: false, error: error.message || 'Unknown 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[1/3] OFFLINE ANALYSIS...' + C.reset);
const offlineFixes = offlineDiagnose(errorText);
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);
});
// 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);
}
// 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.yellow + '[!] Syntax issues remain. Try relaunching or paste the new error.' + C.reset);
return { success: false };
}
};
// Interactive mode
const runInteractive = async () => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (q) => new Promise(resolve => rl.question(q, resolve));
while (true) {
banner();
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 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...');
continue;
}
const result = await attemptRepair(errorText);
console.log('');
if (result.success) {
console.log(C.green + C.bold + ' ╔═══════════════════════════════════════════╗' + C.reset);
console.log(C.green + C.bold + ' ║ ✅ REPAIR COMPLETE ✅ ║' + C.reset);
console.log(C.green + C.bold + ' ╚═══════════════════════════════════════════╝' + C.reset);
console.log(C.cyan + '\n Try launching the TUI again!' + C.reset);
} else {
console.log(C.yellow + ' If the error persists, paste the new error message.' + C.reset);
}
await question('\nPress Enter to continue...');
}
};
// Entry point
runInteractive().catch(e => {
console.error(C.red + 'Smart Repair crashed: ' + e.message + C.reset);
process.exit(1);
});