AI-Repo-Commander/src/main.js

264 lines
10 KiB
JavaScript

// ==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==