// ==COMMAND EXECUTOR START== /* global GM_xmlhttpRequest */ /* global GM_notification */ (function () { /** * @typedef {Object} RepoCommand * @property {string} action * @property {string} [repo] * @property {string} [owner] * @property {string} [path] * @property {string} [content] * @property {string} [commit_message] * @property {string} [url] * @property {boolean} [example] */ /** * @typedef {Object} FileEntry * @property {string} [path] * @property {string} [name] */ class CommandExecutor { /** * @param {RepoCommand} command * @param {Element} sourceElement * @param {string} [label] */ static async execute(command, sourceElement, label = '') { const log = window.AI_REPO_LOGGER; const cfg = window.AI_REPO_CONFIG; try { log.command(command.action, 'executing', { repo: command.repo, path: command.path, label }); if (['update_file', 'create_file'].includes(command.action) && !command.commit_message) { command.commit_message = `AI Repo Commander: ${command.path} (${new Date().toISOString()})`; log.trace('Auto-generated commit message', { message: command.commit_message }); } if (!cfg.get('api.enabled')) { log.warn('API disabled, using mock execution', { action: command.action }); await this.delay(300); const result = this._success({ status: 200, responseText: JSON.stringify({ success: true, message: 'Mock execution completed' }) }, command, sourceElement, true, label); log.command(command.action, 'complete', { mock: true }); return result; } log.verbose('Making API request', { action: command.action, url: command.url, label }); const res = await this._api(command); log.verbose('API request succeeded', { action: command.action, status: res.status }); const result = this._success(res, command, sourceElement, false, label); log.command(command.action, 'complete', { repo: command.repo, path: command.path }); return result; } catch (err) { log.command(command.action, 'error', { error: err.message }); log.error('Command execution failed', { action: command.action, error: err.message, stack: err.stack?.slice(0, 150) }); return this._error(err, command, sourceElement, label); } } static _api(command, attempt = 0) { const cfg = window.AI_REPO_CONFIG; const log = window.AI_REPO_LOGGER; const maxRetries = cfg.get('api.maxRetries') ?? 2; const timeout = cfg.get('api.timeout') ?? 60000; const bridgeKey = this._getBridgeKey(); if (attempt > 0) { log.warn(`Retrying API call (attempt ${attempt + 1}/${maxRetries + 1})`, { action: command.action }); } log.trace('GM_xmlhttpRequest details', { method: 'POST', url: command.url, timeout, hasKey: !!bridgeKey, attempt: attempt + 1 }); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: command.url, headers: { 'X-Bridge-Key': bridgeKey, 'Content-Type': 'application/json' }, data: JSON.stringify(command), timeout, onload: (r) => { if (r.status >= 200 && r.status < 300) { log.trace('API response received', { status: r.status, bodyLength: r.responseText?.length }); resolve(r); } else { log.error(`API returned error status`, { status: r.status, statusText: r.statusText }); reject(new Error(`API Error ${r.status}: ${r.statusText}`)); } }, onerror: (e) => { if (attempt < maxRetries) { const delay = 1000 * (attempt + 1); log.warn(`Network error, retrying in ${delay}ms`, { error: e?.error || 'unknown' }); setTimeout(() => this._api(command, attempt + 1).then(resolve).catch(reject), delay); } else { log.error('Network error, max retries exceeded', { attempts: attempt + 1, error: e?.error || 'unknown' }); reject(new Error(`Network error after ${attempt + 1} attempts: ${e?.error || 'unknown'}`)); } }, ontimeout: () => { log.error('API request timed out', { timeout, action: command.action }); reject(new Error(`API timeout after ${timeout}ms`)); } }); }); } static _getBridgeKey() { const cfg = window.AI_REPO_CONFIG; const log = window.AI_REPO_LOGGER; let key = cfg.get('api.bridgeKey'); if (!key) { log.warn('Bridge key not found, prompting user'); key = prompt('[AI Repo Commander] Enter your bridge key for this session:') || ''; if (!key) { log.error('User did not provide bridge key'); throw new Error('Bridge key required when API is enabled'); } if (confirm('Save this bridge key to avoid future prompts?')) { cfg.set('api.bridgeKey', key); log.info('Bridge key saved to config'); } else { log.info('Bridge key accepted for this session only'); } } else { log.trace('Using saved bridge key from config'); } return key; } static _success(response, command, el, isMock = false, label = '') { let data; try { data = JSON.parse(response.responseText || '{}'); } catch { data = { message: 'Operation completed' }; } this._status(el, isMock ? 'MOCK' : 'SUCCESS', { action: command.action, details: data.message || 'Completed successfully', label }); if (command.action === 'get_file') this._handleGetFile(data, label); if (command.action === 'list_files') this._handleListFiles(data, label); return { success: true, data, isMock }; } static _error(error, command, el, label = '') { this._status(el, 'ERROR', { action: command.action, details: error.message, label }); return { success: false, error: error.message }; } static _status(el, type, data) { const div = document.createElement('div'); div.style.cssText = ` padding:8px 12px;margin:8px 0;border-radius:4px; border-left:4px solid ${this._color(type)}; background:rgba(255,255,255,.05);font-family:monospace;font-size:13px;white-space:pre-wrap; `; div.textContent = `${data.label || data.action} — ${type}${data.details ? ': ' + data.details : ''}`; el.appendChild(div); } static _color(t){ return ({SUCCESS:'#10B981', ERROR:'#EF4444', MOCK:'#8B5CF6'})[t] || '#6B7280'; } static _handleGetFile(data, label) { const log = window.AI_REPO_LOGGER; const content = data?.content?.data ?? data?.content ?? data?.result?.content?.data ?? data?.result?.content; if (!content) { log.warn('get_file response missing content field'); return; } window.AI_REPO_RESPONSES = window.AI_REPO_RESPONSES || []; window.AI_REPO_RESPONSES.push({ label, content }); log.verbose('File content stored for paste-back', { label, contentLength: content.length }); } static _handleListFiles(data, label) { const log = window.AI_REPO_LOGGER; /** @type {Array} */ const files = data?.files ?? data?.result?.files; if (!Array.isArray(files)) { log.warn('list_files response missing files array'); return; } const listing = '```text\n' + files.map(f => (typeof f === 'string' ? f : (f?.path || f?.name || JSON.stringify(f)))).join('\n') + '\n```'; window.AI_REPO_RESPONSES = window.AI_REPO_RESPONSES || []; window.AI_REPO_RESPONSES.push({ label, content: listing }); log.verbose('File listing stored for paste-back', { label, fileCount: files.length }); } static delay(ms) { return new Promise(r => setTimeout(r, ms)); } } window.AI_REPO_EXECUTOR = CommandExecutor; })(); // ==COMMAND EXECUTOR END==