// ==MAIN START== // Module: main.js // Purpose: Legacy entry point and convenience API exposure. // - Initializes observer and optionally scans existing messages // - Exposes window.AI_REPO with pause/resume/clearHistory helpers // Note: detector.js implements the primary monitoring pipeline; this module // remains for compatibility and console convenience. (function () { 'use strict'; if (!window.AI_REPO_CONFIG || !window.AI_REPO_LOGGER || !window.AI_REPO_HISTORY || !window.AI_REPO_PARSER || !window.AI_REPO_EXECUTOR) { console.error('AI Repo Commander: Core modules not loaded'); return; } const logger = window.AI_REPO_LOGGER; const config = window.AI_REPO_CONFIG; const history = window.AI_REPO_HISTORY; class AIRepoCommander { constructor() { this.isInitialized = false; this.observer = null; this.processed = new WeakSet(); this.messageSelectors = [ '[data-message-author-role="assistant"]', '.chat-message:not([data-message-author-role="user"])', '.message-content' ]; } initialize() { if (this.isInitialized) { logger.warn('Already initialized, skipping'); return; } logger.info('AI Repo Commander initializing', { version: config.get('meta.version'), debugLevel: config.get('debug.level'), apiEnabled: config.get('api.enabled') }); logger.verbose('Configuration summary', { debounceDelay: config.get('execution.debounceDelay'), queueMaxPerMin: config.get('queue.maxPerMinute'), autoSubmit: config.get('ui.autoSubmit'), processExisting: config.get('ui.processExisting') }); this.startObserver(); if (config.get('ui.processExisting')) { logger.verbose('Will process existing messages on page'); this.scanExisting(); } this.exposeAPI(); this.isInitialized = true; logger.info('AI Repo Commander initialized'); logger.trace('Exposed globals:', Object.keys(window).filter(k => k.startsWith('AI_REPO'))); } startObserver() { this.observer = new MutationObserver((mutations) => { if (config.get('runtime.paused')) { logger.trace('Mutations ignored (paused)'); return; } let assistantMsgCount = 0; for (const m of mutations) { if (m.type !== 'childList') continue; for (const n of m.addedNodes) { if (n.nodeType !== 1) continue; if (this.isAssistantMessage(n)) { assistantMsgCount++; this.processMessage(n); } const inner = n.querySelectorAll?.(this.messageSelectors.join(',')) || []; inner.forEach(el => { if (this.isAssistantMessage(el)) { assistantMsgCount++; this.processMessage(el); } }); } } if (assistantMsgCount > 0) { logger.verbose(`Detected ${assistantMsgCount} assistant message(s)`); } }); this.observer.observe(document.body, { childList: true, subtree: true }); logger.verbose('MutationObserver started, watching document.body'); } isAssistantMessage(el) { return this.messageSelectors.some(sel => el.matches?.(sel)); } processMessage(el) { if (this.processed.has(el)) { logger.trace('Message already processed, skipping'); return; } const commands = this.extractCommands(el); if (!commands.length) { logger.trace('No commands found in message'); return; } logger.verbose(`Found ${commands.length} command block(s) in message`); this.processed.add(el); const maxPerMsg = config.get('queue.maxPerMessage'); const toProcess = commands.slice(0, maxPerMsg); if (commands.length > maxPerMsg) { logger.warn(`Message has ${commands.length} commands, limiting to first ${maxPerMsg}`); } toProcess.forEach((cmdText, idx) => { if (history.isProcessed(el, idx)) { logger.verbose(`Command #${idx + 1} already executed, adding retry button`); this.addRetryButton(el, cmdText, idx); } else { logger.verbose(`Queueing command #${idx + 1} for execution`); void this.run(el, cmdText, idx); } }); } extractCommands(el) { const text = el.textContent || ''; const out = []; const re = /@bridge@[\s\S]*?@end@/g; let m; while ((m = re.exec(text)) !== null) out.push(m[0]); return out; } async run(el, commandText, index) { try { logger.trace(`Starting run() for command #${index + 1}`, { preview: commandText.slice(0, 60) + '...' }); history.markProcessed(el, index); const parsed = window.AI_REPO_PARSER.parse(commandText); logger.verbose(`Parsed command #${index + 1}:`, { action: parsed.action, repo: parsed.repo, path: parsed.path }); const validation = window.AI_REPO_PARSER.validate(parsed); if (!validation.isValid) { logger.error('Command validation failed', { errors: validation.errors, command: parsed.action }); this.addRetryButton(el, commandText, index); return; } if (validation.example) { logger.info('Skipping example command', { action: parsed.action }); return; } const debounce = config.get('execution.debounceDelay') || 0; if (debounce > 0) { logger.trace(`Debouncing for ${debounce}ms before execution`); await this.delay(debounce); } const label = `Command ${index + 1}`; logger.verbose(`Executing command #${index + 1}: ${parsed.action}`); await window.AI_REPO_EXECUTOR.execute(parsed, el, label); logger.verbose(`Command #${index + 1} completed successfully`); } catch (e) { logger.error('Command execution failed', { error: e.message, stack: e.stack?.slice(0, 200), commandIndex: index }); this.addRetryButton(el, commandText, index); } } addRetryButton(el, commandText, idx) { const btn = document.createElement('button'); btn.textContent = `Run Again #${idx + 1}`; btn.style.cssText = ` padding:4px 8px;margin:4px;border:1px solid #374151;border-radius:4px; background:#1f2937;color:#e5e7eb;cursor:pointer; `; btn.addEventListener('click', () => this.run(el, commandText, idx)); el.appendChild(btn); } scanExisting() { const nodes = document.querySelectorAll(this.messageSelectors.join(',')); logger.verbose(`Scanning ${nodes.length} existing message(s) on page`); let processed = 0; nodes.forEach(el => { if (this.isAssistantMessage(el)) { processed++; this.processMessage(el); } }); logger.info(`Scanned ${processed} existing assistant message(s)`); } exposeAPI() { // Public API (short name) window.AI_REPO = { version: config.get('meta.version'), config: config, logger: logger, history, pause: () => { config.set('runtime.paused', true); logger.info('Paused'); }, resume: () => { config.set('runtime.paused', false); logger.info('Resumed'); }, clearHistory: () => { history.clear(); logger.info('History cleared'); } }; // Emergency STOP function window.AI_REPO_STOP = () => { config.set('api.enabled', false); config.set('runtime.paused', true); const queuedCount = window.AI_REPO_QUEUE?.size?.() || 0; window.AI_REPO_QUEUE?.clear?.(); logger.error(`🚨 EMERGENCY STOP: cancelled ${queuedCount} queued command(s)`); logger.error('API disabled and scanning paused'); }; // Bridge key setter window.AI_REPO_SET_KEY = function(k) { if (typeof k === 'string' && k.trim()) { config.set('api.bridgeKey', k.trim()); logger.info('Bridge key updated'); return true; } logger.warn('Invalid bridge key'); return false; }; } delay(ms) { return new Promise(r => setTimeout(r, ms)); } destroy() { this.observer?.disconnect(); this.processed = new WeakSet(); this.isInitialized = false; logger.info('AI Repo Commander destroyed'); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { window.AI_REPO_MAIN = new AIRepoCommander(); window.AI_REPO_MAIN.initialize(); }); } else { window.AI_REPO_MAIN = new AIRepoCommander(); window.AI_REPO_MAIN.initialize(); // Kick off the advanced detector (restores settle/debounce, multi-block, cluster rescan) window.AI_REPO_DETECTOR?.start(); } })(); // ==MAIN END==