// ==UserScript== // @name AI Repo Commander // @namespace http://tampermonkey.net/ // @version 1.6.2 // @description Execute @bridge@ YAML commands from AI assistants (safe & robust): complete-block detection, streaming-settle, persistent dedupe, paste+autosubmit, debug console with Tools/Settings, draggable/collapsible panel, multi-command queue // @author Your Name // @match https://chat.openai.com/* // @match https://chatgpt.com/* // @match https://claude.ai/* // @match https://gemini.google.com/* // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_setClipboard // @connect n8n.brrd.tech // ==/UserScript== (function () { 'use strict'; // ---------------------- Storage keys ---------------------- const STORAGE_KEYS = { history: 'ai_repo_commander_executed', cfg: 'ai_repo_commander_cfg', panel: 'ai_repo_commander_panel_state' }; // ---------------------- Config (with persistence) ---------------------- const DEFAULT_CONFIG = { ENABLE_API: true, DEBUG_MODE: true, DEBUG_LEVEL: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace DEBUG_WATCH_MS: 120000, DEBUG_MAX_LINES: 400, DEBUG_SHOW_PANEL: true, // Timing & API // If you see "debouncing → error" in logs (assistant streams very slowly), // try bumping DEBOUNCE_DELAY by +1000–2000 and/or SETTLE_CHECK_MS by +400–800. DEBOUNCE_DELAY: 3000, MAX_RETRIES: 2, VERSION: '1.6.2', API_TIMEOUT_MS: 60000, PROCESS_EXISTING: false, ASSISTANT_ONLY: true, BRIDGE_KEY: '', // Persistent dedupe window DEDUPE_TTL_MS: 30 * 24 * 60 * 60 * 1000, // 30 days COLD_START_MS: 2000, SHOW_EXECUTED_MARKER: true, // Housekeeping CLEANUP_AFTER_MS: 30000, CLEANUP_INTERVAL_MS: 60000, // Paste + submit behavior APPEND_TRAILING_NEWLINE: true, AUTO_SUBMIT: true, POST_PASTE_DELAY_MS: 250, SUBMIT_MODE: 'button_first', // Streaming-complete hardening // SETTLE_CHECK_MS is the "stable window" after last text change; // SETTLE_POLL_MS is how often we re-check the code block. REQUIRE_TERMINATOR: true, SETTLE_CHECK_MS: 800, SETTLE_POLL_MS: 200, // Runtime toggles RUNTIME: { PAUSED: false }, // New additions for hardening STUCK_AFTER_MS: 10 * 60 * 1000, SCAN_DEBOUNCE_MS: 250, FAST_WARN_MS: 50, SLOW_WARN_MS: 60_000, // Queue management QUEUE_MIN_DELAY_MS: 800, QUEUE_MAX_PER_MINUTE: 15, QUEUE_MAX_PER_MESSAGE: 5, QUEUE_WAIT_FOR_COMPOSER_MS: 6000, }; function loadSavedConfig() { try { const raw = localStorage.getItem(STORAGE_KEYS.cfg); if (!raw) return structuredClone(DEFAULT_CONFIG); const saved = JSON.parse(raw); const merged = { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } }; return merged; } catch { return structuredClone(DEFAULT_CONFIG); } } function saveConfig(cfg) { try { localStorage.setItem(STORAGE_KEYS.cfg, JSON.stringify(cfg)); } catch {} } const CONFIG = loadSavedConfig(); // ---------------------- Debug Console ---------------------- let RC_DEBUG = null; class DebugConsole { constructor(cfg) { this.cfg = cfg; this.buf = []; this.loopCounts = new Map(); this.startedAt = Date.now(); this.panel = null; this.bodyLogs = null; this.bodyTools = null; this.collapsed = false; this.drag = { active: false, dx: 0, dy: 0 }; this.panelState = this._loadPanelState(); this.loopCleanupInterval = setInterval(() => { if (Date.now() - this.startedAt > this.cfg.DEBUG_WATCH_MS * 2) { this.loopCounts.clear(); this.startedAt = Date.now(); this.verbose('Cleaned loop counters'); } }, this.cfg.DEBUG_WATCH_MS); if (cfg.DEBUG_SHOW_PANEL) this.mount(); this.info(`Debug console ready (level=${cfg.DEBUG_LEVEL})`); } _loadPanelState() { try { return JSON.parse(localStorage.getItem(STORAGE_KEYS.panel) || '{}'); } catch { return {}; } } _savePanelState(partial) { try { const merged = { ...(this.panelState || {}), ...(partial || {}) }; this.panelState = merged; localStorage.setItem(STORAGE_KEYS.panel, JSON.stringify(merged)); } catch {} } // Levels error(msg, data) { this._log(1, 'ERROR', msg, data); } warn(msg, data) { this._log(2, 'WARN', msg, data); } info(msg, data) { this._log(3, 'INFO', msg, data); } verbose(msg, data){ this._log(4, 'VERB', msg, data); } trace(msg, data) { this._log(5, 'TRACE', msg, data); } command(action, status, extra={}) { const icon = { detected:'👁️', parsing:'📝', validating:'✓', debouncing:'⏳', executing:'⚙️', complete:'✅', error:'❌' }[status] || '•'; this.info(`${icon} ${action} [${status}]`, extra); } nowIso() { return new Date().toISOString(); } withinWatch() { return Date.now() - this.startedAt <= this.cfg.DEBUG_WATCH_MS; } logLoop(kind, msg) { const k = `${kind}:${msg}`; const cur = this.loopCounts.get(k) || 0; if (!this.withinWatch() && kind !== 'WARN') return; if (cur >= 10) return; this.loopCounts.set(k, cur + 1); const suffix = (cur + 1) > 1 ? ` (${cur + 1}x)` : ''; if (kind === 'ERROR') this.error(`${msg}${suffix}`); else if (kind === 'WARN') this.warn(`${msg}${suffix}`); else this.info(`${msg}${suffix}`); } copyLast(n=50) { const lines = this.buf.slice(-n).map(e => `${e.ts} ${e.level.padEnd(5)} ${e.msg}${e.data? ' ' + JSON.stringify(e.data): ''}`).join('\n'); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(lines).then(() => { this.info(`Copied last ${Math.min(n, this.buf.length)} lines to clipboard`); }).catch(e => this._fallbackCopy(lines, e)); } else { this._fallbackCopy(lines); } } _fallbackCopy(text, originalError = null) { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); if (ok) this.info(`Copied last ${text.split('\n').length} lines to clipboard (fallback)`); else this.warn('Clipboard copy failed (fallback)'); } catch (e) { this.warn('Clipboard copy failed', { error: originalError?.message || e.message }); } } setLevel(n) { const lv = Math.max(0, Math.min(4, n)); this.cfg.DEBUG_LEVEL = lv; saveConfig(this.cfg); this.info(`Log level => ${lv}`); } _sanitize(data) { if (!data) return null; try { if (data instanceof HTMLElement) return '[HTMLElement]'; if (typeof data === 'string' && data.length > 400) return data.slice(0,400)+'…'; if (typeof data === 'object') { const clone = { ...data }; if (clone.element instanceof HTMLElement) clone.element = '[HTMLElement]'; return clone; } } catch {} return data; } _log(numericLevel, levelName, msg, data) { if (!this.cfg.DEBUG_MODE) return; const thresholdMap = { 0: 0, 1: 1, 2: 3, 3: 4, 4: 5 }; const threshold = thresholdMap[this.cfg.DEBUG_LEVEL] ?? 0; if (numericLevel > threshold) return; const entry = { ts: this.nowIso(), level: levelName, msg: String(msg), data: this._sanitize(data) }; this.buf.push(entry); if (this.buf.length > this.cfg.DEBUG_MAX_LINES) this.buf.splice(0, this.buf.length - this.cfg.DEBUG_MAX_LINES); if (this.cfg.DEBUG_LEVEL >= 3) { const prefix = `[AI RC]`; if (entry.data != null) console.log(prefix, entry.level, entry.msg, entry.data); else console.log(prefix, entry.level, entry.msg); } if (this.panel) this._renderRow(entry); } _renderRow(e) { if (!this.bodyLogs) return; const row = document.createElement('div'); row.style.cssText = 'padding:4px 0;border-bottom:1px dashed #2a2a34;white-space:pre-wrap;word-break:break-word;'; row.textContent = `${e.ts} ${e.level.padEnd(5)} ${e.msg}${e.data? ' ' + JSON.stringify(e.data): ''}`; this.bodyLogs.appendChild(row); while (this.bodyLogs.children.length > this.cfg.DEBUG_MAX_LINES) this.bodyLogs.firstChild.remove(); this.bodyLogs.scrollTop = this.bodyLogs.scrollHeight; } flashBtn(btn, label = 'Done', ms = 900) { if (!btn) return; const old = btn.textContent; btn.disabled = true; btn.textContent = `${label} ✓`; btn.style.opacity = '0.7'; setTimeout(() => { btn.disabled = false; btn.textContent = old; btn.style.opacity = ''; }, ms); } toast(msg, ms = 1200) { if (!this.panel) return; const t = document.createElement('div'); t.textContent = msg; t.style.cssText = ` position:absolute; right:12px; bottom:12px; padding:6px 10px; background:#111827; color:#e5e7eb; border:1px solid #374151; border-radius:6px; font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; opacity:.98; pointer-events:none; box-shadow:0 6px 20px rgba(0,0,0,.35) `; this.panel.appendChild(t); setTimeout(() => t.remove(), ms); } mount() { if (!document.body) { setTimeout(() => this.mount(), 100); return; } const root = document.createElement('div'); root.style.cssText = ` position: fixed; ${this.panelState.left!==undefined ? `left:${this.panelState.left}px; top:${this.panelState.top}px;` : 'right:16px; bottom:16px;'} z-index: 2147483647; width: 460px; max-height: 55vh; display: flex; flex-direction: column; background: rgba(20,20,24,0.92); border:1px solid #3b3b46; border-radius: 8px; color:#e5e7eb; font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; box-shadow: 0 16px 40px rgba(0,0,0,0.55); backdrop-filter: blur(4px); `; root.innerHTML = `
AI Repo Commander
`; document.body.appendChild(root); this.panel = root; this.bodyLogs = root.querySelector('.rc-body-logs'); this.bodyTools = root.querySelector('.rc-body-tools'); // Controls const sel = root.querySelector('.rc-level'); sel.value = String(this.cfg.DEBUG_LEVEL); sel.addEventListener('change', () => this.setLevel(parseInt(sel.value,10))); root.querySelector('.rc-copy').addEventListener('click', (e) => { this.copyLast(50); this.flashBtn(e.currentTarget, 'Copied'); this.toast('Copied last 50 logs'); }); root.querySelector('.rc-copy-200').addEventListener('click', (e) => { this.copyLast(200); this.flashBtn(e.currentTarget, 'Copied'); this.toast('Copied last 200 logs'); }); const pauseBtn = root.querySelector('.rc-pause'); pauseBtn.addEventListener('click', () => { const wasPaused = this.cfg.RUNTIME.PAUSED; this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED; saveConfig(this.cfg); pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause'; pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : ''; pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : ''; this.flashBtn(pauseBtn, this.cfg.RUNTIME.PAUSED ? 'Paused' : 'Resumed'); this.toast(this.cfg.RUNTIME.PAUSED ? 'Paused scanning' : 'Resumed scanning'); this.info(`Runtime ${this.cfg.RUNTIME.PAUSED ? 'paused' : 'resumed'}`); // When RESUMING: start a short cold-start window and mark current hits as processed. if (wasPaused && !this.cfg.RUNTIME.PAUSED) { if (commandMonitor) { commandMonitor.coldStartUntil = Date.now() + (CONFIG.COLD_START_MS || 2000); } markExistingHitsAsProcessed(); } }); // Queue clear button const queueBtn = root.querySelector('.rc-queue-clear'); queueBtn.addEventListener('click', (e) => { window.AI_REPO_QUEUE?.clear?.(); this.flashBtn(e.currentTarget, 'Cleared'); this.toast('Queue cleared'); this.warn('Command queue cleared'); }); root.querySelector('.rc-stop').addEventListener('click', (e) => { window.AI_REPO_STOP?.(); this.flashBtn(e.currentTarget, 'Stopped'); this.toast('Emergency STOP activated'); this.warn('Emergency STOP activated'); }); // Tabs const tabLogs = root.querySelector('.rc-tab-logs'); const tabTools = root.querySelector('.rc-tab-tools'); const selectTab = (tools=false) => { this.bodyLogs.style.display = tools ? 'none' : 'block'; this.bodyTools.style.display = tools ? 'block' : 'none'; tabLogs.style.background = tools ? '#111827' : '#1f2937'; tabTools.style.background = tools ? '#1f2937' : '#111827'; }; tabLogs.addEventListener('click', () => selectTab(false)); tabTools.addEventListener('click', () => { selectTab(true); root.querySelectorAll('.rc-toggle').forEach(inp => { const key = inp.dataset.key; inp.checked = !!this.cfg[key]; }); root.querySelectorAll('.rc-num').forEach(inp => { inp.value = String(this.cfg[inp.dataset.key] ?? ''); }); const dump = JSON.parse(JSON.stringify(this.cfg)); if (dump.BRIDGE_KEY) dump.BRIDGE_KEY = '•'.repeat(8); root.querySelector('.rc-json').value = JSON.stringify(dump, null, 2); const bridgeKeyInput = root.querySelector('.rc-bridge-key'); if (bridgeKeyInput) bridgeKeyInput.value = this.cfg.BRIDGE_KEY ? '•'.repeat(8) : ''; }); // Collapse const collapseBtn = root.querySelector('.rc-collapse'); const setCollapsed = (c) => { this.collapsed = c; this.bodyLogs.style.display = c ? 'none' : 'block'; this.bodyTools.style.display = 'none'; collapseBtn.textContent = c ? '▸' : '▾'; this._savePanelState({ collapsed: c }); }; setCollapsed(!!this.panelState.collapsed); collapseBtn.addEventListener('click', () => setCollapsed(!this.collapsed)); // Dragging const header = root.querySelector('.rc-header'); header.addEventListener('mousedown', (e) => { if ((e.target).closest('button,select,input,textarea,label')) return; this.drag.active = true; const rect = root.getBoundingClientRect(); this.drag.dx = e.clientX - rect.left; this.drag.dy = e.clientY - rect.top; root.style.right = 'auto'; root.style.bottom = 'auto'; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); const onMove = (e) => { if (!this.drag.active) return; const x = Math.max(0, Math.min(window.innerWidth - this.panel.offsetWidth, e.clientX - this.drag.dx)); const y = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - this.drag.dy)); this.panel.style.left = `${x}px`; this.panel.style.top = `${y}px`; }; const onUp = () => { if (!this.drag.active) return; this.drag.active = false; this._savePanelState({ left: parseInt(this.panel.style.left||'0',10), top: parseInt(this.panel.style.top||'0',10) }); document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; // Tools: Clear History root.querySelector('.rc-clear-history').addEventListener('click', (e) => { try { commandMonitor?.history?.resetAll?.(); RC_DEBUG?.info('Conversation history cleared'); GM_notification({ title: 'AI Repo Commander', text: 'Execution marks cleared', timeout: 2500 }); } catch { localStorage.removeItem(STORAGE_KEYS.history); RC_DEBUG?.info('Legacy history key cleared'); } this.flashBtn(e.currentTarget, 'Cleared'); this.toast('Conversation marks cleared'); }); // Tools: toggles & numbers root.querySelectorAll('.rc-toggle').forEach(inp => { const key = inp.dataset.key; inp.checked = !!this.cfg[key]; inp.addEventListener('change', () => { this.cfg[key] = !!inp.checked; saveConfig(this.cfg); this.toast(`${key} = ${this.cfg[key] ? 'on' : 'off'}`); this.info(`Config ${key} => ${this.cfg[key]}`); }); }); root.querySelectorAll('.rc-num').forEach(inp => { inp.value = String(this.cfg[inp.dataset.key] ?? ''); inp.addEventListener('change', () => { const v = parseInt(inp.value, 10); if (!Number.isNaN(v)) { this.cfg[inp.dataset.key] = v; saveConfig(this.cfg); this.toast(`${inp.dataset.key} = ${v}`); this.info(`Config ${inp.dataset.key} => ${v}`); } }); }); // Tools: JSON input root.querySelector('.rc-save-json').addEventListener('click', (e) => { try { const raw = root.querySelector('.rc-json').value; const parsed = JSON.parse(raw); if (Object.prototype.hasOwnProperty.call(parsed, 'BRIDGE_KEY')) { const v = (parsed.BRIDGE_KEY ?? '').toString().trim(); if (v && !/^•+$/.test(v)) { this.cfg.BRIDGE_KEY = v; BRIDGE_KEY = v; } delete parsed.BRIDGE_KEY; } Object.assign(this.cfg, parsed); saveConfig(this.cfg); const dump = JSON.parse(JSON.stringify(this.cfg)); if (dump.BRIDGE_KEY) dump.BRIDGE_KEY = '•'.repeat(8); root.querySelector('.rc-json').value = JSON.stringify(dump, null, 2); this.flashBtn(e.currentTarget, 'Saved'); this.toast('Config saved'); this.info('Config JSON saved'); } catch (err) { this.toast('Invalid JSON', 1500); this.warn('Invalid JSON in config textarea', { error: String(err) }); } }); root.querySelector('.rc-reset-defaults').addEventListener('click', (e) => { Object.assign(this.cfg, structuredClone(DEFAULT_CONFIG)); saveConfig(this.cfg); BRIDGE_KEY = null; const bridgeKeyInput = root.querySelector('.rc-bridge-key'); if (bridgeKeyInput) bridgeKeyInput.value = ''; this.flashBtn(e.currentTarget, 'Reset'); this.toast('Defaults restored'); this.info('Config reset to defaults'); }); // Set initial UI states pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause'; if (this.cfg.RUNTIME.PAUSED) { pauseBtn.style.background = '#f59e0b'; pauseBtn.style.color = '#111827'; } // Bridge Key handlers const bridgeKeyInput = root.querySelector('.rc-bridge-key'); bridgeKeyInput.value = this.cfg.BRIDGE_KEY ? '•'.repeat(8) : ''; root.querySelector('.rc-save-bridge-key').addEventListener('click', (e) => { const raw = (bridgeKeyInput.value || '').trim(); if (/^•+$/.test(raw)) { this.info('Bridge key unchanged'); GM_notification({ title: 'AI Repo Commander', text: 'Bridge key unchanged', timeout: 2000 }); return; } this.cfg.BRIDGE_KEY = raw; saveConfig(this.cfg); BRIDGE_KEY = raw || null; bridgeKeyInput.value = this.cfg.BRIDGE_KEY ? '•'.repeat(8) : ''; this.flashBtn(e.currentTarget, 'Saved'); this.toast('Bridge key saved'); this.info('Bridge key saved (masked)'); GM_notification({ title: 'AI Repo Commander', text: 'Bridge key saved', timeout: 2500 }); }); root.querySelector('.rc-clear-bridge-key').addEventListener('click', (e) => { this.cfg.BRIDGE_KEY = ''; bridgeKeyInput.value = ''; saveConfig(this.cfg); BRIDGE_KEY = null; this.flashBtn(e.currentTarget, 'Cleared'); this.toast('Bridge key cleared'); this.info('Bridge key cleared'); GM_notification({ title: 'AI Repo Commander', text: 'Bridge key cleared', timeout: 2500 }); }); } destroy() { try { clearInterval(this.loopCleanupInterval); } catch {} if (this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel); } } // ---------------------- Platform selectors ---------------------- const PLATFORM_SELECTORS = { 'chat.openai.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' }, 'chatgpt.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' }, 'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' }, 'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' } }; // ---------------------- Fingerprinting helpers (portable) ---------------------- const MSG_SELECTORS = [ '[data-message-author-role]', // ChatGPT/OpenAI '.chat-message', // Claude '.message-content' // Gemini ]; // Consistent DJB2 variant function _hash(s) { let h = 5381; for (let i = 0; i < s.length; i++) h = ((h << 5) + h) ^ s.charCodeAt(i); return (h >>> 0).toString(36); } // Normalize text a bit to reduce spurious diffs function _norm(s) { return (s || '') .replace(/\r/g, '') .replace(/\u200b/g, '') // zero-width .replace(/[ \t]+\n/g, '\n') // trailing ws .trim(); } // Extract the *command block* if present; else fall back to element text function _commandishText(el) { // Mirror parser's detector: require header, action, and '@end@' const blocks = el.querySelectorAll('pre code, pre, code'); for (const b of blocks) { const t = _norm(b.textContent || ''); if (/@end@\s*$/m.test(t) && /(^|\n)\s*@bridge@\b/m.test(t) && /(^|\n)\s*action\s*:/m.test(t)) { return t; } } // No command block found; use element text (capped) return _norm((el.textContent || '').slice(0, 2000)); } // Hash of the command (or element text) capped to 2000 chars function _hashCommand(el) { const t = _commandishText(el); return _hash(t.slice(0, 2000)); } // Hash of the *preceding context*: concatenate previous messages' text until ~2000 chars function _hashPrevContext(el) { const all = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))); const idx = all.indexOf(el); if (idx <= 0) return '0'; let remaining = 2000; let buf = ''; for (let i = idx - 1; i >= 0 && remaining > 0; i--) { const t = _norm(all[i].textContent || ''); if (!t) continue; // Prepend last slice so the nearest history weighs most const take = t.slice(-remaining); buf = take + buf; remaining -= take.length; } // Hash only the *last* 2000 chars of the collected buffer (stable, compact) return _hash(buf.slice(-2000)); } // Ordinal among messages that share the same (commandHash, prevCtxHash) function _ordinalForKey(el, key) { const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))); let n = 0; for (const node of list) { const nodeKey = node === el ? key : (() => { // Compute on the fly only if needed const ch = _hashCommand(node); const ph = _hashPrevContext(node); return `ch:${ch}|ph:${ph}`; })(); if (nodeKey === key) n++; if (node === el) return n; // 1-based ordinal } return 1; } // DOM path hint for extra fingerprint stability function _domHint(node) { const p = []; let n = node; for (let i = 0; n && i < 4; i++) { // last 4 ancestors const tag = (n.tagName || '').toLowerCase(); const cls = (n.className || '').toString().split(/\s+/).slice(0, 2).join('.'); p.push(tag + (cls ? '.' + cls : '')); n = n.parentElement; } return p.join('>'); } // Main fingerprint function function fingerprintElement(el) { // Always use content-based fingerprinting for reliability across reloads const ch = _hashCommand(el); const ph = _hashPrevContext(el); const dh = _hash(_domHint(el)); const key = `ch:${ch}|ph:${ph}`; const n = _ordinalForKey(el, key); const fingerprint = `${key}|hint:${dh}|n:${n}`; RC_DEBUG?.trace('Generated fingerprint', { fingerprint: fingerprint.slice(0, 60) + '...', commandHash: ch, prevContextHash: ph, domHint: dh, ordinal: n }); return fingerprint; } // ---------------------- Multi-block extraction helpers ---------------------- function extractAllCompleteBlocks(text) { const out = []; const re = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/gm; let m; while ((m = re.exec(text)) !== null) { const inner = (m[1] || '').trimEnd(); if (inner && /(^|\n)\s*action\s*:/m.test(inner)) out.push(inner); } return out; // array of inner texts (without @bridge@/@end@) } function findAllCommandsInMessage(el) { const blocks = el.querySelectorAll('pre code, pre, code'); const hits = []; for (const b of blocks) { const txt = (b.textContent || '').trim(); const parts = extractAllCompleteBlocks(txt); for (const part of parts) hits.push({ blockElement: b, text: `@bridge@\n${part}\n@end@` }); } return hits; } // Tiny badge on the message showing how many got queued function attachQueueBadge(el, count) { if (el.querySelector('.ai-rc-queue-badge')) return; const badge = document.createElement('span'); badge.className = 'ai-rc-queue-badge'; badge.textContent = `${count} command${count>1?'s':''} queued`; badge.style.cssText = ` display:inline-block; padding:2px 6px; margin:4px 0; background:#3b82f6; color:#fff; border-radius:4px; font:11px ui-monospace, monospace;`; el.insertBefore(badge, el.firstChild); } // Wait until it's safe to paste/submit async function waitForComposerReady({ timeoutMs = CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS, pollMs = 200 } = {}) { const start = Date.now(); while (Date.now() - start < timeoutMs) { // basic "assistant still typing" check const lastMsg = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))).pop(); if (lastMsg?.querySelector?.('[aria-busy="true"], .typing-indicator')) { await ExecutionManager.delay(400); continue; } const el = getVisibleInputCandidate(); const btn = findSendButton(); const btnReady = !btn || (!btn.disabled && btn.getAttribute('aria-disabled') !== 'true'); const busy = document.querySelector( '[aria-busy="true"], [data-state="loading"], ' + 'button[disabled], button[aria-disabled="true"]' ); if (el && btnReady && !busy) return true; await ExecutionManager.delay(pollMs); } RC_DEBUG?.warn('Composer not ready within timeout'); return false; } // ---------------------- Conversation-Aware Element History ---------------------- function getConversationId() { const host = location.hostname.replace('chat.openai.com', 'chatgpt.com'); // normalize // ChatGPT if (/chatgpt\.com/.test(host)) { // Try GPT-specific conversation path first: /g/[gpt-id]/c/[conv-id] const gptMatch = location.pathname.match(/\/g\/[^/]+\/c\/([a-f0-9-]+)/i); if (gptMatch?.[1]) { RC_DEBUG?.verbose('Conversation ID from GPT URL', { id: gptMatch[1].slice(0, 12) + '...' }); return `chatgpt:${gptMatch[1]}`; } // Regular conversation path: /c/[conv-id] const m = location.pathname.match(/\/c\/([a-f0-9-]+)/i); if (m?.[1]) { RC_DEBUG?.verbose('Conversation ID from URL path', { id: m[1].slice(0, 12) + '...' }); return `chatgpt:${m[1]}`; } // Try embedded page state (best-effort) try { const scripts = document.querySelectorAll('script[type="application/json"]'); for (const s of scripts) { const t = s.textContent || ''; const j = /"conversationId":"([a-f0-9-]+)"/i.exec(t); if (j?.[1]) { RC_DEBUG?.verbose('Conversation ID from page state', { id: j[1].slice(0, 12) + '...' }); return `chatgpt:${j[1]}`; } } } catch {} return `chatgpt:${location.pathname || '/'}`; } // Claude if (/claude\.ai/.test(host)) { const m1 = location.pathname.match(/\/chat\/([^/]+)/i); const m2 = location.pathname.match(/\/thread\/([^/]+)/i); const id = (m1?.[1] || m2?.[1]); if (id) { RC_DEBUG?.verbose('Conversation ID (Claude)', { id: id.slice(0, 12) + '...' }); } return `claude:${id || (location.pathname || '/')}`; } // Gemini / others const generic = `${host}:${location.pathname || '/'}`; RC_DEBUG?.verbose('Conversation ID (generic)', { id: generic }); return generic; } // ---------------------- Command requirements ---------------------- const REQUIRED_FIELDS = { 'get_file': ['action', 'repo', 'path'], 'update_file': ['action', 'repo', 'path', 'content'], 'create_file': ['action', 'repo', 'path', 'content'], 'delete_file': ['action', 'repo', 'path'], 'list_files': ['action', 'repo', 'path'], 'create_repo': ['action', 'repo'], 'create_branch': ['action', 'repo', 'branch'], 'create_pr': ['action', 'repo', 'title', 'head', 'base'], 'merge_pr': ['action', 'repo', 'pr_number'], 'close_pr': ['action', 'repo', 'pr_number'], 'create_issue': ['action', 'repo', 'title'], 'comment_issue': ['action', 'repo', 'issue_number', 'body'], 'close_issue': ['action', 'repo', 'issue_number'], 'rollback': ['action', 'repo', 'commit_sha'], 'create_tag': ['action', 'repo', 'tag', 'target'], 'create_release': ['action', 'repo', 'tag_name', 'name'] }; const FIELD_VALIDATORS = { repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v), path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'), action: (v) => Object.keys(REQUIRED_FIELDS).includes(v), owner: (v) => !v || /^[\w\-]+$/.test(v), url: (v) => !v || /^https?:\/\/.+\..+/.test(v), branch: (v) => v && v.length > 0 && !v.includes('..'), source_branch:(v) => !v || (v.length > 0 && !v.includes('..')), head: (v) => v && v.length > 0, base: (v) => v && v.length > 0, pr_number: (v) => !isNaN(parseInt(v)) && parseInt(v) > 0, issue_number: (v) => !isNaN(parseInt(v)) && parseInt(v) > 0, commit_sha: (v) => /^[a-f0-9]{7,40}$/i.test(v), tag: (v) => v && v.length > 0 && !v.includes(' '), tag_name: (v) => v && v.length > 0 && !v.includes(' '), target: (v) => v && v.length > 0, title: (v) => v && v.length > 0, name: (v) => v && v.length > 0, body: (v) => typeof v === 'string', message: (v) => typeof v === 'string' }; const STATUS_TEMPLATES = { SUCCESS: '[{action}: Success] {details}', ERROR: '[{action}: Error] {details}', VALIDATION_ERROR: '[{action}: Invalid] {details}', EXECUTING: '[{action}: Processing...]', MOCK: '[{action}: Mock] {details}' }; const COMMAND_STATES = { DETECTED: 'detected', PARSING: 'parsing', VALIDATING: 'validating', DEBOUNCING: 'debouncing', EXECUTING: 'executing', COMPLETE: 'complete', ERROR: 'error' }; // ---------------------- Persistent Command History ---------------------- class ConvHistory { constructor() { this.convId = getConversationId(); this.key = `ai_rc:conv:${this.convId}:processed`; this.session = new Set(); this.cache = this._load(); this._cleanupTTL(); RC_DEBUG?.info('ConvHistory initialized', { convId: this.convId.slice(0, 50) + (this.convId.length > 50 ? '...' : ''), cachedCount: Object.keys(this.cache).length }); } _load() { try { return JSON.parse(localStorage.getItem(this.key) || '{}'); } catch { return {}; } } _save() { try { localStorage.setItem(this.key, JSON.stringify(this.cache)); } catch {} } _cleanupTTL() { const ttl = CONFIG.DEDUPE_TTL_MS || (30 * 24 * 60 * 60 * 1000); const now = Date.now(); let dirty = false; for (const [fp, ts] of Object.entries(this.cache)) { if (!ts || (now - ts) > ttl) { delete this.cache[fp]; dirty = true; } } if (dirty) { this._save(); RC_DEBUG?.verbose('Cleaned expired fingerprints from cache'); } } hasElement(el, suffix = '') { let fp = fingerprintElement(el); if (suffix) fp += `#${suffix}`; const result = this.session.has(fp) || (fp in this.cache); if (result && CONFIG.DEBUG_LEVEL >= 4) { RC_DEBUG?.trace('Element already processed', { fingerprint: fp.slice(0, 60) + '...', inSession: this.session.has(fp), inCache: fp in this.cache }); } return result; } markElement(el, suffix = '') { let fp = fingerprintElement(el); if (suffix) fp += `#${suffix}`; this.session.add(fp); this.cache[fp] = Date.now(); this._save(); RC_DEBUG?.verbose('Marked element as processed', { fingerprint: fp.slice(0, 60) + '...' }); if (CONFIG.SHOW_EXECUTED_MARKER) { try { el.style.borderLeft = '3px solid #10B981'; el.title = 'Command executed — use "Run again" to re-run'; } catch {} } } unmarkElement(el, suffix = '') { let fp = fingerprintElement(el); if (suffix) fp += `#${suffix}`; this.session.delete(fp); if (fp in this.cache) { delete this.cache[fp]; this._save(); } RC_DEBUG?.verbose('Unmarked element', { fingerprint: fp.slice(0, 60) + '...' }); } resetAll() { this.session.clear(); localStorage.removeItem(this.key); this.cache = {}; RC_DEBUG?.info('All conversation history cleared'); } } // Global helpers (stable) window.AI_REPO = { clearHistory: () => { try { commandMonitor?.history?.resetAll?.(); } catch {} localStorage.removeItem(STORAGE_KEYS.history); // legacy }, getConfig: () => structuredClone(CONFIG), setConfig: (patch) => { Object.assign(CONFIG, patch||{}); saveConfig(CONFIG); }, }; function attachRunAgainUI(containerEl, onRun) { if (containerEl.querySelector('.ai-rc-rerun')) return; const bar = document.createElement('div'); bar.className = 'ai-rc-rerun'; bar.style.cssText = 'margin:8px 0; display:flex; gap:8px; align-items:center;'; const msg = document.createElement('span'); msg.textContent = 'Already executed.'; msg.style.cssText = 'flex:1; font-size:13px; opacity:.9;'; const run = document.createElement('button'); run.textContent = 'Run again'; run.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#1f2937; color:#e5e7eb;'; const dismiss = document.createElement('button'); dismiss.textContent = 'Dismiss'; dismiss.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#111827; color:#9ca3af;'; run.onclick = (ev) => { RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Running'); onRun(); }; dismiss.onclick = (ev) => { RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Dismissed'); bar.remove(); }; bar.append(msg, run, dismiss); containerEl.appendChild(bar); } // When resuming from pause, treat like a cold start & mark all currently-visible commands as processed. // Adds "Run again" buttons so nothing auto-executes. function markExistingHitsAsProcessed() { try { const messages = document.querySelectorAll(MSG_SELECTORS.join(',')); messages.forEach((el) => { const hits = findAllCommandsInMessage(el); if (!hits.length) return; el.dataset.aiRcProcessed = '1'; const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE); capped.forEach((_, idx) => { commandMonitor?.history?.markElement?.(el, idx + 1); }); attachRunAgainUI(el, () => { const nowHits = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE); nowHits.forEach((h, i) => commandMonitor.enqueueCommand(el, h, i)); }); }); RC_DEBUG?.info('Resume-safe guard: marked visible commands as processed & attached Run again buttons'); } catch (e) { RC_DEBUG?.warn('Resume-safe guard failed', { error: String(e) }); } } // ---------------------- UI feedback ---------------------- class UIFeedback { static appendStatus(sourceElement, templateType, data) { const statusElement = this.createStatusElement(templateType, data); const existing = sourceElement.querySelector('.ai-repo-commander-status'); if (existing) existing.remove(); sourceElement.appendChild(statusElement); } static createStatusElement(templateType, data) { const template = STATUS_TEMPLATES[templateType] || STATUS_TEMPLATES.ERROR; const message = template.replace('{action}', data.action).replace('{details}', data.details); const el = document.createElement('div'); el.className = 'ai-repo-commander-status'; el.textContent = message; el.style.cssText = ` padding: 8px 12px; margin: 10px 0; border-radius: 4px; border-left: 4px solid ${this.color(templateType)}; background-color: rgba(255,255,255,0.08); font-family: monospace; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid rgba(255,255,255,0.15); `; return el; } static color(t) { const c = { SUCCESS:'#10B981', ERROR:'#EF4444', VALIDATION_ERROR:'#F59E0B', EXECUTING:'#3B82F6', MOCK:'#8B5CF6' }; return c[t] || '#6B7280'; } } // ---------------------- Paste + Submit helpers ---------------------- function getVisibleInputCandidate() { const candidates = [ '.ProseMirror#prompt-textarea', '#prompt-textarea.ProseMirror', '#prompt-textarea', '.ProseMirror', '[contenteditable="true"]', 'textarea' ]; for (const sel of candidates) { const el = document.querySelector(sel); if (!el) continue; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') continue; if (el.offsetParent === null && style.position !== 'fixed') continue; return el; } return null; } function findSendButton() { const selectors = [ 'button[data-testid="send-button"]', 'button[aria-label*="Send"]', 'button[aria-label*="send"]', 'button[aria-label*="Submit"]', 'button[aria-label*="submit"]', 'form button[type="submit"]' ]; for (const s of selectors) { const btn = document.querySelector(s); if (!btn) continue; const style = window.getComputedStyle(btn); const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true'; if (style.display === 'none' || style.visibility === 'hidden') continue; if (btn.offsetParent === null && style.position !== 'fixed') continue; if (!disabled) return btn; } return null; } function pressEnterOn(el) { const events = ['keydown','keypress','keyup']; for (const type of events) { const ok = el.dispatchEvent(new KeyboardEvent(type, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true })); if (!ok) return false; } return true; } async function submitComposer() { try { const btn = findSendButton(); if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) { btn.click(); return true; } const el = getVisibleInputCandidate(); if (!el) return false; return pressEnterOn(el); } catch { return false; } } function pasteToComposer(text) { RC_DEBUG?.info('🔵 pasteToComposer CALLED', { textLength: text.length, preview: text.substring(0, 100) }); try { const el = getVisibleInputCandidate(); if (!el) { RC_DEBUG?.warn('❌ No input element found'); GM_notification({ title: 'AI Repo Commander', text: 'No input box found to paste file content.', timeout: 4000 }); return false; } RC_DEBUG?.verbose('Found input element', { tagName: el.tagName, classList: Array.from(el.classList || []).join(' '), contentEditable: el.getAttribute('contenteditable') }); const payload = CONFIG.APPEND_TRAILING_NEWLINE ? (text.endsWith('\n') ? text : text + '\n') : text; el.focus(); // Method 1: ClipboardEvent try { const dt = new DataTransfer(); dt.setData('text/plain', payload); const pasteEvt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }); const dispatched = el.dispatchEvent(pasteEvt); const notPrevented = !pasteEvt.defaultPrevented; RC_DEBUG?.verbose('ClipboardEvent attempt', { dispatched, notPrevented }); if (dispatched && notPrevented) { RC_DEBUG?.info('✅ Paste method succeeded: ClipboardEvent'); return true; } } catch (e) { RC_DEBUG?.verbose('ClipboardEvent failed', { error: String(e) }); } // Method 2: ProseMirror const isPM = el.classList && el.classList.contains('ProseMirror'); if (isPM) { RC_DEBUG?.verbose('Attempting ProseMirror paste'); // Pad with blank lines before/after to preserve ``` fences visually. const payload2 = `\n${payload.replace(/\n?$/, '\n')}\n`; const escape = (s) => s.replace(/&/g,'&').replace(//g,'>'); const html = String(payload2) .split('\n') .map(line => line.length ? `

${escape(line)}

` : '


') .join(''); el.innerHTML = html; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); RC_DEBUG?.info('✅ Paste method succeeded: ProseMirror'); return true; } // Method 3: execCommand try { const sel = window.getSelection && window.getSelection(); if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) { const r = document.createRange(); r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r); RC_DEBUG?.verbose('Selection range set for execCommand'); } const success = document.execCommand && document.execCommand('insertText', false, payload); RC_DEBUG?.verbose('execCommand attempt', { success }); if (success) { RC_DEBUG?.info('✅ Paste method succeeded: execCommand'); return true; } } catch (e) { RC_DEBUG?.verbose('execCommand failed', { error: String(e) }); } // Method 4: TEXTAREA/INPUT if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { RC_DEBUG?.verbose('Attempting TEXTAREA/INPUT paste'); el.value = payload; el.dispatchEvent(new Event('input', { bubbles: true })); RC_DEBUG?.info('✅ Paste method succeeded: TEXTAREA/INPUT'); return true; } // Method 5: contentEditable if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') { RC_DEBUG?.verbose('Attempting contentEditable paste'); el.textContent = payload; el.dispatchEvent(new Event('input', { bubbles: true })); RC_DEBUG?.info('✅ Paste method succeeded: contentEditable'); return true; } // Fallback: GM_setClipboard RC_DEBUG?.warn('All paste methods failed, trying GM_setClipboard fallback'); try { if (typeof GM_setClipboard === 'function') { GM_setClipboard(payload, { type: 'text', mimetype: 'text/plain' }); GM_notification({ title: 'AI Repo Commander', text: 'Content copied to clipboard — press Ctrl/Cmd+V to paste.', timeout: 5000 }); RC_DEBUG?.info('✅ Paste method succeeded: GM_setClipboard (manual paste required)'); } } catch (e) { RC_DEBUG?.warn('GM_setClipboard failed', { error: String(e) }); } return false; } catch (e) { RC_DEBUG?.warn('pasteToComposer fatal error', { error: String(e) }); return false; } } async function pasteAndMaybeSubmit(text) { const ready = await waitForComposerReady({ timeoutMs: CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS }); if (!ready) { RC_DEBUG?.warn('Composer not ready; re-queueing paste'); execQueue.push(async () => { await pasteAndMaybeSubmit(text); }); return false; } const pasted = pasteToComposer(text); if (!pasted) return false; try { const el = getVisibleInputCandidate(); const actualContent = el?.textContent || el?.value || '[no content found]'; RC_DEBUG?.info('📋 Content in composer after paste', { expectedLength: text.length, actualLength: actualContent.length, actualPreview: actualContent.substring(0, 200) }); } catch (e) { RC_DEBUG?.warn('Could not read composer content', { error: String(e) }); } if (!CONFIG.AUTO_SUBMIT) return true; await ExecutionManager.delay(CONFIG.POST_PASTE_DELAY_MS); const ok = await submitComposer(); if (!ok) { GM_notification({ title: 'AI Repo Commander', text: 'Pasted content, but auto-submit did not trigger.', timeout: 4000 }); } return true; } // ---------------------- Parser (strict, require @end@) ---------------------- class CommandParser { static parseYAMLCommand(codeBlockText) { const block = this.extractCompleteBlock(codeBlockText); if (!block) throw new Error('No complete @bridge@ command found (missing @end@ terminator).'); const parsed = this.parseKeyValuePairs(block); // Defaults parsed.url = parsed.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge'; parsed.owner = parsed.owner || 'rob'; // Helpful default: create_branch without source_branch defaults to 'main' if (parsed.action === 'create_branch' && !parsed.source_branch) { parsed.source_branch = 'main'; } // Expand owner/repo shorthand if (parsed.repo && typeof parsed.repo === 'string' && parsed.repo.includes('/')) { const [owner, repo] = parsed.repo.split('/', 2); if (!parsed.owner) parsed.owner = owner; parsed.repo = repo; } return parsed; } static extractCompleteBlock(text) { // Require terminator @end@ (clearer than --- which appears in markdown) const pattern = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/m; const m = text.match(pattern); if (!m) return null; const inner = m[1]?.trimEnd(); if (!inner) return null; return inner; } static parseKeyValuePairs(block) { const lines = block.split('\n'); const result = {}; let currentKey = null; let collecting = false; let buf = []; for (const raw of lines) { const line = raw.replace(/\r$/, ''); if (collecting) { const looksKey = /^[A-Za-z_][\w\-]*\s*:/.test(line); const unindented = !/^[ \t]/.test(line); // End the current '|' block on ANY unindented key, not just a small whitelist if (looksKey && unindented) { result[currentKey] = buf.join('\n').trimEnd(); collecting = false; buf = []; } else { buf.push(line); continue; } } const idx = line.indexOf(':'); if (idx !== -1) { const key = line.slice(0, idx).trim(); let value = line.slice(idx + 1).trim(); if (value === '|') { currentKey = key; collecting = true; buf = []; } else if (value === '') { currentKey = key; result[key] = ''; } else { result[key] = value; currentKey = null; } } else if (currentKey && result[currentKey] === '') { result[currentKey] += (result[currentKey] ? '\n' : '') + line.trimEnd(); } } if (collecting && currentKey) result[currentKey] = buf.join('\n').trimEnd(); return result; } static validateStructure(parsed) { const errors = []; // Example commands are treated as valid but inert const isExample = parsed.example === true || parsed.example === 'true' || String(parsed.example || '').toLowerCase() === 'yes'; const action = parsed.action; if (isExample) { return { isValid: true, errors, example: true }; } if (!action) { errors.push('Missing required field: action'); return { isValid:false, errors }; } const req = REQUIRED_FIELDS[action]; if (!req) { errors.push(`Unknown action: ${action}`); return { isValid:false, errors }; } for (const f of req) if (!parsed[f] && parsed[f] !== '') errors.push(`Missing required field: ${f}`); for (const [field, value] of Object.entries(parsed)) { const validator = FIELD_VALIDATORS[field]; if (validator && !validator(value)) errors.push(`Invalid format for field: ${field}`); } return { isValid: errors.length === 0, errors }; } } // ---------------------- Execution ---------------------- class ExecutionManager { static async executeCommand(command, sourceElement) { try { if ((command.action === 'update_file' || command.action === 'create_file') && !command.commit_message) { command.commit_message = `AI Repo Commander: ${command.path} (${new Date().toISOString()})`; } if (!CONFIG.ENABLE_API) return this.mockExecution(command, sourceElement); UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Making API request...' }); const res = await this.makeAPICallWithRetry(command); return this.handleSuccess(res, command, sourceElement); } catch (error) { return this.handleError(error, command, sourceElement); } } static async makeAPICallWithRetry(command, attempt = 0) { try { requireBridgeKeyIfNeeded(); return await this.makeAPICall(command); } catch (err) { if (attempt < CONFIG.MAX_RETRIES) { await this.delay(1000 * (attempt + 1)); return this.makeAPICallWithRetry(command, attempt + 1); } const totalAttempts = attempt + 1; throw new Error(`${err.message} (failed after ${totalAttempts} attempts; max ${CONFIG.MAX_RETRIES + 1})`); } } static makeAPICall(command) { return new Promise((resolve, reject) => { const bridgeKey = requireBridgeKeyIfNeeded(); GM_xmlhttpRequest({ method: 'POST', url: command.url, headers: { 'X-Bridge-Key': bridgeKey, 'Content-Type': 'application/json' }, data: JSON.stringify(command), timeout: CONFIG.API_TIMEOUT_MS || 60000, onload: (response) => { if (response.status >= 200 && response.status < 300) { return resolve(response); } const body = response.responseText ? ` body=${response.responseText.slice(0,300)}` : ''; reject(new Error(`API Error ${response.status}: ${response.statusText}${body}`)); }, onerror: (error) => { const msg = (error && (error.error || error.message)) ? (error.error || error.message) : JSON.stringify(error ?? {}); reject(new Error(`Network error: ${msg}`)); }, ontimeout: () => reject(new Error(`API request timeout after ${CONFIG.API_TIMEOUT_MS}ms`)) }); }); } static async mockExecution(command, sourceElement) { await this.delay(500); const mock = { status: 200, responseText: JSON.stringify({ success: true, message: `Mock execution completed for ${command.action}`, data: { command: command.action, repo: command.repo, path: command.path, commit_message: command.commit_message } }) }; return this.handleSuccess(mock, command, sourceElement, true); } static _extractGetFileBody(payload) { const item = Array.isArray(payload) ? payload[0] : payload; return ( item?.result?.content?.data ?? item?.content?.data ?? payload?.result?.content?.data ?? null ); } static _extractFilesArray(payload) { const obj = Array.isArray(payload) ? payload[0] : payload; let files = obj?.result?.files ?? obj?.files ?? null; if (!files) { const res = obj?.result; if (res) { for (const [k, v] of Object.entries(res)) { if (Array.isArray(v) && v.length && (k.toLowerCase().includes('file') || typeof v[0] === 'string' || v[0]?.path || v[0]?.name)) { files = v; break; } } } } return Array.isArray(files) ? files : null; } static _formatFilesListing(files) { const pickPath = (f) => { if (typeof f === 'string') return f; if (typeof f?.path === 'string') return f.path; if (f?.dir && f?.name) return `${f.dir.replace(/\/+$/,'')}/${f.name}`; if (f?.name) return f.name; try { return JSON.stringify(f); } catch { return String(f); } }; const lines = files.map(pickPath).filter(Boolean).sort(); return '```text\n' + lines.join('\n') + '\n```'; } static async handleSuccess(response, command, sourceElement, isMock = false) { let data; try { data = JSON.parse(response.responseText || '{}'); } catch { data = { message: 'Operation completed (no JSON body)' }; } UIFeedback.appendStatus(sourceElement, isMock ? 'MOCK' : 'SUCCESS', { action: command.action, details: data.message || 'Operation completed successfully' }); if (command.action === 'get_file') { const body = this._extractGetFileBody(data); if (typeof body === 'string' && body.length) { await pasteAndMaybeSubmit(body); } else { GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 }); } } if (command.action === 'list_files') { const files = this._extractFilesArray(data); if (files && files.length) { const listing = this._formatFilesListing(files); await pasteAndMaybeSubmit(listing); } else { const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```'; await pasteAndMaybeSubmit(fallback); GM_notification({ title: 'AI Repo Commander', text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.', timeout: 5000 }); } } return { success: true, data, isMock }; } static handleError(error, command, sourceElement) { UIFeedback.appendStatus(sourceElement, 'ERROR', { action: command.action || 'Command', details: error.message }); return { success: false, error: error.message }; } static delay(ms) { return new Promise(r => setTimeout(r, ms)); } } // ---------------------- Execution Queue ---------------------- class ExecutionQueue { constructor({ minDelayMs = CONFIG.QUEUE_MIN_DELAY_MS, maxPerMinute = CONFIG.QUEUE_MAX_PER_MINUTE } = {}) { this.q = []; this.running = false; this.minDelayMs = minDelayMs; this.maxPerMinute = maxPerMinute; this.timestamps = []; this.onSizeChange = null; } push(task) { this.q.push(task); this.onSizeChange?.(this.q.length); if (!this.running) this._drain(); } clear() { this.q.length = 0; this.onSizeChange?.(0); } cancelOne(predicate) { const i = this.q.findIndex(predicate); if (i >= 0) this.q.splice(i, 1); this.onSizeChange?.(this.q.length); } _withinBudget() { const now = Date.now(); this.timestamps = this.timestamps.filter(t => now - t < 60_000); return this.timestamps.length < this.maxPerMinute; } async _drain() { if (this.running) return; this.running = true; const origLen = this.q.length; while (this.q.length) { // rate cap while (!this._withinBudget()) await ExecutionManager.delay(500); const fn = this.q.shift(); this.onSizeChange?.(this.q.length); RC_DEBUG?.toast?.(`Executing command ${origLen - this.q.length}/${origLen}`, 800); try { await fn(); } catch { /* error already surfaced */ } this.timestamps.push(Date.now()); await ExecutionManager.delay(this.minDelayMs); } this.running = false; } } const execQueue = new ExecutionQueue(); window.AI_REPO_QUEUE = { clear: () => execQueue.clear(), size: () => execQueue.q.length, cancelOne: (cb) => execQueue.cancelOne(cb), }; // ---------------------- Bridge Key ---------------------- let BRIDGE_KEY = null; function requireBridgeKeyIfNeeded() { if (!CONFIG.ENABLE_API) return BRIDGE_KEY; // 1) Try runtime if (BRIDGE_KEY && typeof BRIDGE_KEY === 'string' && BRIDGE_KEY.length) { return BRIDGE_KEY; } // 2) Try saved config if (CONFIG.BRIDGE_KEY && typeof CONFIG.BRIDGE_KEY === 'string' && CONFIG.BRIDGE_KEY.length) { BRIDGE_KEY = CONFIG.BRIDGE_KEY; RC_DEBUG?.info('Using saved bridge key from config'); return BRIDGE_KEY; } // 3) Prompt fallback const entered = prompt( '[AI Repo Commander] Enter your bridge key for this session (or set it in Tools → Bridge Configuration to avoid this prompt):' ); if (!entered) throw new Error('Bridge key required when API is enabled.'); BRIDGE_KEY = entered; // Offer to save for next time try { if (confirm('Save this bridge key in Settings → Bridge Configuration to avoid future prompts?')) { CONFIG.BRIDGE_KEY = BRIDGE_KEY; saveConfig(CONFIG); RC_DEBUG?.info('Bridge key saved to config'); } } catch { /* ignore */ } return BRIDGE_KEY; } // Optional: expose a safe setter for console use (won't log the key) window.AI_REPO_SET_KEY = function setBridgeKey(k) { BRIDGE_KEY = (k || '').trim() || null; if (BRIDGE_KEY) { RC_DEBUG?.info('Bridge key set for this session'); } else { RC_DEBUG?.info('Bridge key cleared for this session'); } }; // ---------------------- Monitor (with streaming "settle" & complete-block check) ---------------------- class CommandMonitor { constructor() { this.trackedMessages = new Map(); this.history = new ConvHistory(); this.coldStartUntil = Date.now() + (CONFIG.COLD_START_MS || 0); this.observer = null; this.currentPlatform = null; this._idCounter = 0; this.cleanupIntervalId = null; this.initialize(); } getReadableMessageId(element) { this._idCounter += 1; const id = `cmd-${this._idCounter}-${Math.random().toString(36).slice(2,6)}`; if (element?.dataset) element.dataset.aiRcId = id; return id; } extractAction(text) { const m = /(^|\n)\s*action\s*:\s*([A-Za-z_][\w\-]*)/m.exec(text || ''); return m ? m[2] : 'unknown'; } initialize() { this.detectPlatform(); this.startObservation(); this.setupEmergencyStop(); RC_DEBUG?.info('AI Repo Commander initialized', { ENABLE_API: CONFIG.ENABLE_API, DEBUG_MODE: CONFIG.DEBUG_MODE, DEBOUNCE_DELAY: CONFIG.DEBOUNCE_DELAY, MAX_RETRIES: CONFIG.MAX_RETRIES, VERSION: CONFIG.VERSION }); if (CONFIG.ENABLE_API) { if (CONFIG.BRIDGE_KEY) { RC_DEBUG?.info('API is enabled — using saved bridge key from config'); } else { RC_DEBUG?.warn('API is enabled — you will be prompted for your bridge key on first command.'); } } this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS); // Wire up queue size updates to UI if (execQueue) { execQueue.onSizeChange = (n) => { const queueBtn = document.querySelector('.rc-queue-clear'); if (queueBtn) queueBtn.textContent = `Clear Queue (${n})`; }; } } detectPlatform() { const host = window.location.hostname; this.currentPlatform = PLATFORM_SELECTORS[host] || PLATFORM_SELECTORS['chat.openai.com']; } startObservation() { let scanPending = false; let lastScan = 0; let lastMessageCount = 0; const scheduleScan = () => { if (scanPending) return; scanPending = true; const delay = Math.max(0, CONFIG.SCAN_DEBOUNCE_MS - (Date.now() - lastScan)); setTimeout(() => { scanPending = false; lastScan = Date.now(); this.scanMessages(); }, delay); }; // MutationObserver for immediate detection - watching edits AND additions this.observer = new MutationObserver((mutations) => { let shouldScan = false; let reasons = new Set(); for (const m of mutations) { // A) New nodes under the DOM (previous fast path) if (m.type === 'childList') { for (const node of m.addedNodes) { if (node.nodeType !== 1) continue; if (node.matches?.('pre, code') || node.querySelector?.('pre, code')) { reasons.add('code block added'); shouldScan = true; break; } // Also consider new assistant messages appearing if (node.matches?.(this.currentPlatform.messages) || node.querySelector?.(this.currentPlatform.messages)) { reasons.add('message added'); shouldScan = true; break; } } } // B) Text *inside* existing nodes changed (critical for streaming) if (m.type === 'characterData') { const el = m.target?.parentElement; if (!el) continue; if (el.closest?.('pre, code') || el.closest?.(this.currentPlatform.messages)) { reasons.add('text changed'); shouldScan = true; } } // C) Attribute toggles that show/hide or "finalize" code if (m.type === 'attributes') { const target = m.target; if (target?.matches?.('pre, code') || target?.closest?.(this.currentPlatform.messages)) { reasons.add('attribute changed'); shouldScan = true; } } if (shouldScan) break; } if (shouldScan) { RC_DEBUG?.trace('MO: scan triggered', { reasons: Array.from(reasons).join(', ') }); scheduleScan(); } }); // Observe all changes - no attributeFilter to catch any streaming-related attrs this.observer.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true }); // Polling fallback - scan every 2 seconds if message count changed this.pollingInterval = setInterval(() => { if (CONFIG.RUNTIME.PAUSED) return; const messages = document.querySelectorAll(this.currentPlatform.messages); const currentCount = messages.length; if (currentCount !== lastMessageCount) { RC_DEBUG?.trace('Polling detected message count change', { old: lastMessageCount, new: currentCount }); lastMessageCount = currentCount; scheduleScan(); } }, 2000); if (CONFIG.PROCESS_EXISTING) { setTimeout(() => { RC_DEBUG?.info('Initial scan after page load (PROCESS_EXISTING=true)'); this.scanMessages(); }, 600); } else { RC_DEBUG?.info('Initial scan skipped (PROCESS_EXISTING=false)'); } } isCompleteCommandText(txt) { if (!CONFIG.REQUIRE_TERMINATOR) return /(^|\n)\s*@bridge@\b/m.test(txt) && /(^|\n)\s*action\s*:/m.test(txt); return /(^|\n)\s*@bridge@\b/m.test(txt) && /(^|\n)\s*action\s*:/m.test(txt) && /@end@\s*$/m.test(txt); } findCommandInCodeBlock(el) { const blocks = el.querySelectorAll('pre code, pre, code'); for (const b of blocks) { const txt = (b.textContent || '').trim(); if (this.isCompleteCommandText(txt)) { return { blockElement: b, text: txt }; } } return null; } scanMessages() { if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.logLoop('loop', 'scan paused'); return; } const messages = document.querySelectorAll(this.currentPlatform.messages); let skipped = 0, found = 0; messages.forEach((el) => { if (!this.isAssistantMessage(el)) return; if (el.dataset.aiRcProcessed) return; const hits = findAllCommandsInMessage(el); if (!hits.length) return; if (hits.length === 1) { el.dataset.aiRcProcessed = '1'; if (this.history.hasElement(el, 1)) { attachRunAgainUI(el, () => this.trackMessage(el, hits[0].text, this.getReadableMessageId(el))); return; } this.history.markElement(el, 1); this.trackMessage(el, hits[0].text, this.getReadableMessageId(el)); return; } const withinColdStart = Date.now() < this.coldStartUntil; const alreadyAll = hits.every((_, i) => this.history.hasElement(el, i + 1)); RC_DEBUG?.trace('Evaluating message', { withinColdStart, alreadyAll, commandCount: hits.length }); // Skip if cold start or already processed (but DON'T mark new ones in history during cold start) if (withinColdStart || alreadyAll) { el.dataset.aiRcProcessed = '1'; RC_DEBUG?.verbose('Skipping command(s) - ' + (withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'), { fingerprint: fingerprintElement(el).slice(0, 40) + '...', commandCount: hits.length }); attachRunAgainUI(el, () => { el.dataset.aiRcProcessed = '1'; const hit2 = findAllCommandsInMessage(el); if (hit2.length) { const capped = hit2.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE); capped.forEach((h, i) => this.enqueueCommand(el, h, i)); } }); skipped += hits.length; return; } // New message that hasn't been executed → auto-execute once el.dataset.aiRcProcessed = '1'; const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE); attachQueueBadge(el, capped.length); capped.forEach((hit, idx) => { // mark each sub-command immediately to avoid re-exec on reloads this.history.markElement(el, idx + 1); this.enqueueCommand(el, hit, idx); }); found += capped.length; }); if (skipped) RC_DEBUG?.info(`Skipped ${skipped} command(s) - Run Again buttons added`); if (found) RC_DEBUG?.info(`Auto-executing ${found} new command(s)`); } enqueueCommand(element, hit, idx) { const messageId = this.getReadableMessageId(element); const subId = `${messageId}#${idx + 1}`; // track this sub-command so updateState/attachRetryUI can work this.trackedMessages.set(subId, { element, originalText: hit.text, state: COMMAND_STATES.DETECTED, startTime: Date.now(), lastUpdate: Date.now(), cancelToken: { cancelled: false }, }); execQueue.push(async () => { // optional tiny settle for streaming await ExecutionManager.delay(CONFIG.SETTLE_POLL_MS); const allNow = findAllCommandsInMessage(element); const liveForIdx = allNow[idx]?.text; const finalTxt = (liveForIdx && this.isCompleteCommandText(liveForIdx)) ? liveForIdx : hit.text; let parsed; try { parsed = CommandParser.parseYAMLCommand(finalTxt); const val = CommandParser.validateStructure(parsed); if (!val.isValid) throw new Error(`Validation failed: ${val.errors.join(', ')}`); } catch (err) { UIFeedback.appendStatus(element, 'ERROR', { action: 'Command', details: err.message }); this.attachRetryUI(element, subId); return; } this.updateState(subId, COMMAND_STATES.EXECUTING); const res = await ExecutionManager.executeCommand(parsed, element); if (!res || res.success === false) { this.updateState(subId, COMMAND_STATES.ERROR); this.attachRetryUI(element, subId); return; } this.updateState(subId, COMMAND_STATES.COMPLETE); }); } isAssistantMessage(el) { if (!CONFIG.ASSISTANT_ONLY) return true; const host = location.hostname; if (/chat\.openai\.com|chatgpt\.com/.test(host)) { const roleEl = el.closest?.('[data-message-author-role]') || el; return roleEl?.getAttribute?.('data-message-author-role') === 'assistant'; } if (/claude\.ai/.test(host)) { const isUser = !!el.closest?.('[data-message-author-role="user"]'); return !isUser; } if (/gemini\.google\.com/.test(host)) return true; return true; } trackMessage(element, text, messageId) { RC_DEBUG?.info('New command detected', { messageId, preview: text.substring(0, 120) }); this.trackedMessages.set(messageId, { element, originalText: text, state: COMMAND_STATES.DETECTED, startTime: Date.now(), lastUpdate: Date.now(), cancelToken: { cancelled: false } }); this.updateState(messageId, COMMAND_STATES.PARSING); this.processCommand(messageId); } async debounceWithCancel(messageId) { const start = Date.now(); const delay = CONFIG.DEBOUNCE_DELAY; const checkInterval = 100; while (Date.now() - start < delay) { const msg = this.trackedMessages.get(messageId); if (!msg || msg.cancelToken?.cancelled) return; msg.lastUpdate = Date.now(); this.trackedMessages.set(messageId, msg); await ExecutionManager.delay(Math.min(checkInterval, delay - (Date.now() - start))); } } async waitForStableCompleteBlock(element, initialText, messageId) { let deadline = Date.now() + Math.max(0, CONFIG.SETTLE_CHECK_MS); let last = initialText; while (Date.now() < deadline) { const rec = this.trackedMessages.get(messageId); if (!rec || rec.cancelToken?.cancelled) { RC_DEBUG?.warn('Settle cancelled', { messageId }); return ''; } rec.lastUpdate = Date.now(); this.trackedMessages.set(messageId, rec); await ExecutionManager.delay(CONFIG.SETTLE_POLL_MS); const hit = this.findCommandInCodeBlock(element); const txt = hit ? hit.text : ''; if (!txt || !this.isCompleteCommandText(txt)) { continue; } if (txt === last) { // stable; keep waiting } else { last = txt; deadline = Date.now() + Math.max(0, CONFIG.SETTLE_CHECK_MS); } } const finalHit = this.findCommandInCodeBlock(element); return finalHit ? finalHit.text : ''; } attachRetryUI(element, messageId) { if (element.querySelector('.ai-rc-rerun')) return; attachRunAgainUI(element, () => { element.dataset.aiRcProcessed = '1'; // Parse sub-index from messageId like "...#3" const m = /#(\d+)$/.exec(messageId); const wantIdx = m ? Math.max(0, parseInt(m[1], 10) - 1) : 0; const all = findAllCommandsInMessage(element); const selected = all[wantIdx]?.text || all[0]?.text; if (selected) { this.trackedMessages.delete(messageId); const newId = this.getReadableMessageId(element); this.trackMessage(element, selected, newId); } }); } updateState(messageId, state) { const msg = this.trackedMessages.get(messageId); if (!msg) return; const old = msg.state; msg.state = state; msg.lastUpdate = Date.now(); this.trackedMessages.set(messageId, msg); RC_DEBUG?.command(this.extractAction(msg.originalText), state, { messageId, transition: `${old} -> ${state}` }); } async processCommand(messageId) { if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.info('process paused, skipping', { messageId }); return; } const started = Date.now(); try { const message = this.trackedMessages.get(messageId); if (!message) { RC_DEBUG?.error('Message not found', { messageId }); return; } if (message.cancelToken?.cancelled) { RC_DEBUG?.warn('Operation cancelled', { messageId }); return; } // 1) Parse let parsed; try { parsed = CommandParser.parseYAMLCommand(message.originalText); } catch (err) { RC_DEBUG?.error(`Command parsing failed: ${err.message}`, { messageId }); this.updateState(messageId, COMMAND_STATES.ERROR); if (/No complete @bridge@/.test(err.message)) return; this.attachRetryUI(message.element, messageId); UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: err.message }); return; } if (message.cancelToken?.cancelled) { RC_DEBUG?.warn('Operation cancelled after parse', { messageId }); return; } // 2) Validate this.updateState(messageId, COMMAND_STATES.VALIDATING); let validation = CommandParser.validateStructure(parsed); // Silently skip examples (already marked in history by the scanner) if (validation.example) { RC_DEBUG?.info('Example command detected — skipping execution'); this.updateState(messageId, COMMAND_STATES.COMPLETE); return; } if (!validation.isValid) { this.attachRetryUI(message.element, messageId); throw new Error(`Validation failed: ${validation.errors.join(', ')}`); } // 3) Debounce this.updateState(messageId, COMMAND_STATES.DEBOUNCING); const before = message.originalText; await this.debounceWithCancel(messageId); if (message.cancelToken?.cancelled) { RC_DEBUG?.warn('Operation cancelled after debounce', { messageId }); return; } const stable = await this.waitForStableCompleteBlock(message.element, before, messageId); if (!stable) { this.updateState(messageId, COMMAND_STATES.ERROR); return; } if (stable !== before) { RC_DEBUG?.info('Command changed after settle (re-validate)', { messageId }); message.originalText = stable; const reParsed = CommandParser.parseYAMLCommand(stable); const reVal = CommandParser.validateStructure(reParsed); if (!reVal.isValid) { this.attachRetryUI(message.element, messageId); throw new Error(`Final validation failed: ${reVal.errors.join(', ')}`); } parsed = reParsed; } // 4) Execute this.updateState(messageId, COMMAND_STATES.EXECUTING); const result = await ExecutionManager.executeCommand(parsed, message.element); if (!result || result.success === false) { RC_DEBUG?.warn('Execution reported failure', { messageId }); this.updateState(messageId, COMMAND_STATES.ERROR); this.attachRetryUI(message.element, messageId); return; } const duration = Date.now() - started; if (duration < CONFIG.FAST_WARN_MS) RC_DEBUG?.warn('Command completed very fast', { messageId, duration }); if (duration > CONFIG.SLOW_WARN_MS) RC_DEBUG?.warn('Command took very long', { messageId, duration }); this.updateState(messageId, COMMAND_STATES.COMPLETE); } catch (error) { const duration = Date.now() - started; RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration }); this.updateState(messageId, COMMAND_STATES.ERROR); const message = this.trackedMessages.get(messageId); if (/No complete @bridge@/.test(error.message)) return; if (message) { this.attachRetryUI(message.element, messageId); UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message }); } } } cleanupProcessedCommands() { const now = Date.now(); let count = 0; for (const [id, msg] of this.trackedMessages.entries()) { const age = now - (msg.lastUpdate || msg.startTime || now); const finished = (msg.state === COMMAND_STATES.COMPLETE || msg.state === COMMAND_STATES.ERROR); const shouldCleanup = (finished && age > CONFIG.CLEANUP_AFTER_MS) || (age > CONFIG.STUCK_AFTER_MS); if (shouldCleanup) { if (age > CONFIG.STUCK_AFTER_MS && !finished) { RC_DEBUG?.warn('Cleaning stuck entry', { messageId: id, state: msg.state, age }); } this.trackedMessages.delete(id); count++; } } if (count) RC_DEBUG?.info(`Cleaned ${count} tracked entries`); } stopAllProcessing() { this.trackedMessages.clear(); if (this.observer) this.observer.disconnect(); if (this.pollingInterval) { clearInterval(this.pollingInterval); this.pollingInterval = null; } if (this.cleanupIntervalId) { clearInterval(this.cleanupIntervalId); this.cleanupIntervalId = null; } RC_DEBUG?.destroy?.(); } setupEmergencyStop() { window.AI_REPO_STOP = () => { CONFIG.ENABLE_API = false; CONFIG.RUNTIME.PAUSED = true; saveConfig(CONFIG); const queuedCount = execQueue.q.length; execQueue.clear(); RC_DEBUG?.error(`🚨 EMERGENCY STOP: cancelled ${queuedCount} queued command(s)`); for (const [id, msg] of this.trackedMessages.entries()) { if (msg.cancelToken) msg.cancelToken.cancelled = true; if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) { RC_DEBUG?.error('Emergency stop - cancelling command', { messageId: id }); this.updateState(id, COMMAND_STATES.ERROR); } } this.stopAllProcessing(); RC_DEBUG?.error('🚨 EMERGENCY STOP ACTIVATED 🚨'); GM_notification({ text: 'All command processing stopped', title: 'Emergency Stop', timeout: 5000 }); }; } } // ---------------------- Manual retry helpers ---------------------- let commandMonitor; window.AI_REPO_RETRY_COMMAND_TEXT = () => { RC_DEBUG?.warn('Retry by text is deprecated. Use AI_REPO_RETRY_MESSAGE(messageId) or click "Run again".'); }; window.AI_REPO_RETRY_MESSAGE = (messageId) => { try { const msg = commandMonitor?.trackedMessages?.get(messageId); if (!msg) { RC_DEBUG?.warn('Message not found for retry', { messageId }); return; } msg.element.dataset.aiRcProcessed = '1'; RC_DEBUG?.info('Retrying message now', { messageId }); commandMonitor.updateState(messageId, COMMAND_STATES.PARSING); commandMonitor.processCommand(messageId); } catch (e) { RC_DEBUG?.error('Failed to retry message', { messageId, error: String(e) }); } }; // ---------------------- Test commands ---------------------- const TEST_COMMANDS = { validUpdate: `\ \`\`\`yaml @bridge@ action: update_file repo: test-repo path: TEST.md content: | Test content Multiple lines --- Even markdown horizontal rules work! @end@ \`\`\` `, getFile: `\ \`\`\`yaml @bridge@ action: get_file repo: test-repo path: README.md @end@ \`\`\` `, listFiles: `\ \`\`\`yaml @bridge@ action: list_files repo: test-repo path: . @end@ \`\`\` `, createBranch: `\ \`\`\`yaml @bridge@ action: create_branch repo: test-repo branch: feature/new-feature source_branch: main @end@ \`\`\` `, createPR: `\ \`\`\`yaml @bridge@ action: create_pr repo: test-repo title: Add new feature head: feature/new-feature base: main body: | This PR adds a new feature - Item 1 - Item 2 @end@ \`\`\` `, createIssue: `\ \`\`\`yaml @bridge@ action: create_issue repo: test-repo title: Bug report body: | Description of the bug Steps to reproduce: 1. Step one 2. Step two @end@ \`\`\` `, createTag: `\ \`\`\`yaml @bridge@ action: create_tag repo: test-repo tag: v1.0.0 target: main message: Release version 1.0.0 @end@ \`\`\` `, createRelease: `\ \`\`\`yaml @bridge@ action: create_release repo: test-repo tag_name: v1.0.0 name: Version 1.0.0 body: | ## What's New - Feature A - Feature B ## Bug Fixes - Fix X - Fix Y @end@ \`\`\` `, multiCommand: `\ \`\`\`yaml @bridge@ action: get_file repo: test-repo path: file1.txt @end@ @bridge@ action: get_file repo: test-repo path: file2.txt @end@ @bridge@ action: list_files repo: test-repo path: . @end@ \`\`\` ` }; // ---------------------- Init ---------------------- function initializeRepoCommander() { if (!RC_DEBUG) RC_DEBUG = new DebugConsole(CONFIG); if (!commandMonitor) { commandMonitor = new CommandMonitor(); window.AI_REPO_COMMANDER = { monitor: commandMonitor, config: CONFIG, test: TEST_COMMANDS, version: CONFIG.VERSION, history: commandMonitor.history, submitComposer, queue: execQueue }; RC_DEBUG?.info('AI Repo Commander fully initialized'); RC_DEBUG?.info('API Enabled:', { value: CONFIG.ENABLE_API }); RC_DEBUG?.info('Test commands available in window.AI_REPO_COMMANDER.test'); RC_DEBUG?.info('Reset history with Tools → Clear History or window.AI_REPO.clearHistory()'); RC_DEBUG?.info('Queue management: window.AI_REPO_QUEUE.clear() / .size() / .cancelOne()'); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeRepoCommander); } else { initializeRepoCommander(); } })();