From 945c7eca575007451fb407ff10754b33e43c6f76 Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 6 Oct 2025 18:12:23 +0000 Subject: [PATCH] Update src/ai-repo-commander.user.js --- src/ai-repo-commander.user.js | 638 +++++++++++++++++++++++++++++++--- 1 file changed, 599 insertions(+), 39 deletions(-) diff --git a/src/ai-repo-commander.user.js b/src/ai-repo-commander.user.js index 1d4f499..682cd3d 100644 --- a/src/ai-repo-commander.user.js +++ b/src/ai-repo-commander.user.js @@ -1,40 +1,600 @@ - // ==UserScript== - // @name AI Repo Commander - // @namespace http://violentmonkey.com/ - // @version 1.0.0 - // @description Enable AI assistants to interact with git repositories safely - // @author rob - // @match https://chat.openai.com/* - // @match https://claude.ai/* - // @match https://gemini.google.com/* - // @grant GM_xmlhttpRequest - // @grant GM_notification - // @grant GM_setValue - // @grant GM_getValue - // ==/UserScript== +// ==UserScript== +// @name AI Repo Commander +// @namespace http://tampermonkey.net/ +// @version 1.0.0 +// @description Enable AI assistants to securely interact with git repositories via YAML commands +// @author Your Name +// @match https://chat.openai.com/* +// @match https://claude.ai/* +// @match https://gemini.google.com/* +// @grant GM_xmlhttpRequest +// @grant GM_notification +// ==/UserScript== - /* - * AI Repo Commander - Main Implementation - * Safety-first browser extension for AI-to-repo workflows - */ - - (function() { - 'use strict'; - - // Master configuration - SAFETY FIRST - const CONFIG = { - ENABLE_API: false, // MUST be manually enabled for production use - DEBUG_MODE: true, // Development logging - DEBOUNCE_DELAY: 5000, // 5-second bot typing protection - MAX_RETRIES: 2, // API retry attempts - VERSION: '1.0.0' - }; - - console.log(`🚀 AI Repo Commander v${CONFIG.VERSION} loaded!`); - console.log(`🔒 API Enabled: ${CONFIG.ENABLE_API}`); - console.log(`🐛 Debug Mode: ${CONFIG.DEBUG_MODE}`); - - // TODO: Implementation will go here - // This is the main file structure - - })(); \ No newline at end of file +(function() { + 'use strict'; + + // Configuration - MUST be manually enabled for production + const CONFIG = { + ENABLE_API: false, // Master kill switch + DEBUG_MODE: true, // Development logging + DEBOUNCE_DELAY: 5000, // 5-second bot typing protection + MAX_RETRIES: 2, // API retry attempts + VERSION: '1.0.0' + }; + + // Platform-specific DOM selectors + const PLATFORM_SELECTORS = { + 'chat.openai.com': { + messages: '[class*="message"]', + input: '#prompt-textarea', + content: '[class*="markdown"]' + }, + 'claude.ai': { + messages: '.chat-message', + input: '[contenteditable="true"]', + content: '.content' + }, + 'gemini.google.com': { + messages: '.message-content', + input: 'textarea, [contenteditable="true"]', + content: '.message-text' + } + }; + + // Required fields matrix + const REQUIRED_FIELDS = { + 'update_file': ['action', 'repo', 'path', 'content'], + 'get_file': ['action', 'repo', 'path'], + 'create_repo': ['action', 'repo'], + 'create_file': ['action', 'repo', 'path', 'content'], + 'delete_file': ['action', 'repo', 'path'], + 'list_files': ['action', 'repo', 'path'] + }; + + const FIELD_VALIDATORS = { + 'repo': (value) => /^[\w\-\.]+$/.test(value), + 'path': (value) => !value.includes('..') && !value.startsWith('/'), + 'action': (value) => Object.keys(REQUIRED_FIELDS).includes(value), + 'owner': (value) => !value || /^[\w\-]+$/.test(value), + 'url': (value) => !value || /^https?:\/\/.+\..+/.test(value) + }; + + // Status message templates + const STATUS_TEMPLATES = { + SUCCESS: '[{action}: Success] {details}', + ERROR: '[{action}: Error] {details}', + VALIDATION_ERROR: '[{action}: Invalid] {details}', + EXECUTING: '[{action}: Processing...]', + MOCK: '[{action}: Mock] {details}' + }; + + // Command states + const COMMAND_STATES = { + DETECTED: 'detected', + PARSING: 'parsing', + VALIDATING: 'validating', + DEBOUNCING: 'debouncing', + EXECUTING: 'executing', + COMPLETE: 'complete', + ERROR: 'error' + }; + + // Core Monitor Class + class CommandMonitor { + constructor() { + this.trackedMessages = new Map(); + this.observer = null; + this.currentPlatform = null; + this.initialize(); + } + + initialize() { + this.detectPlatform(); + this.startObservation(); + this.setupEmergencyStop(); + this.log('AI Repo Commander initialized', CONFIG); + } + + detectPlatform() { + const hostname = window.location.hostname; + this.currentPlatform = PLATFORM_SELECTORS[hostname] || PLATFORM_SELECTORS['chat.openai.com']; + } + + startObservation() { + // Observe for new messages + this.observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1) { // Element node + this.scanNode(node); + } + }); + }); + }); + + // Start observing + const targetNode = document.body; + this.observer.observe(targetNode, { + childList: true, + subtree: true + }); + + // Initial scan of existing messages + this.scanExistingMessages(); + } + + scanNode(node) { + if (node.querySelector && node.querySelector(this.currentPlatform.messages)) { + this.scanMessages(); + } + } + + scanExistingMessages() { + setTimeout(() => this.scanMessages(), 1000); + } + + scanMessages() { + const messages = document.querySelectorAll(this.currentPlatform.messages); + + messages.forEach((messageElement) => { + const messageId = this.getMessageId(messageElement); + + // Skip if already tracking this message + if (this.trackedMessages.has(messageId)) { + return; + } + + const text = this.extractText(messageElement); + + if (text && text.includes('^%$bridge')) { + this.trackMessage(messageElement, text, messageId); + } + }); + } + + getMessageId(element) { + return element.id || element.className + '-' + Array.from(element.children).length; + } + + extractText(element) { + const contentElement = element.querySelector(this.currentPlatform.content); + return contentElement ? contentElement.textContent : element.textContent; + } + + trackMessage(element, text, messageId) { + this.log('New command detected:', { messageId, text: text.substring(0, 100) }); + + this.trackedMessages.set(messageId, { + element, + originalText: text, + state: COMMAND_STATES.DETECTED, + startTime: Date.now() + }); + + this.updateState(messageId, COMMAND_STATES.PARSING); + this.processCommand(messageId); + } + + updateState(messageId, state) { + const message = this.trackedMessages.get(messageId); + if (message) { + message.state = state; + message.lastUpdate = Date.now(); + this.trackedMessages.set(messageId, message); + this.log(`Message ${messageId} state updated to: ${state}`); + } + } + + async processCommand(messageId) { + try { + const message = this.trackedMessages.get(messageId); + if (!message) return; + + // Step 1: Parse command + const parsedCommand = CommandParser.parseYAMLCommand(message.originalText); + this.updateState(messageId, COMMAND_STATES.VALIDATING); + + // Step 2: Validate command + const validation = CommandParser.validateStructure(parsedCommand); + if (!validation.isValid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + // Step 3: Debounce (wait for bot to finish typing) + this.updateState(messageId, COMMAND_STATES.DEBOUNCING); + await this.debounce(); + + // Step 4: Execute command + this.updateState(messageId, COMMAND_STATES.EXECUTING); + const result = await ExecutionManager.executeCommand(parsedCommand, message.element); + + // Step 5: Complete + this.updateState(messageId, COMMAND_STATES.COMPLETE); + + } catch (error) { + this.log(`Command processing error: ${error.message}`); + this.updateState(messageId, COMMAND_STATES.ERROR); + + const message = this.trackedMessages.get(messageId); + if (message) { + UIFeedback.replaceWithStatus(message.element, 'ERROR', { + action: 'Command', + details: error.message + }); + } + } + } + + debounce() { + return new Promise(resolve => setTimeout(resolve, CONFIG.DEBOUNCE_DELAY)); + } + + stopAllProcessing() { + this.trackedMessages.clear(); + if (this.observer) { + this.observer.disconnect(); + } + } + + setupEmergencyStop() { + window.AI_REPO_STOP = () => { + CONFIG.ENABLE_API = false; + this.stopAllProcessing(); + this.log('EMERGENCY STOP ACTIVATED'); + GM_notification({ + text: 'AI Repo Commander Emergency Stop Activated', + title: 'Safety System', + timeout: 5000 + }); + }; + } + + log(...args) { + if (CONFIG.DEBUG_MODE) { + console.log('[AI Repo Commander]', ...args); + } + } + } + + // Command Parser Class + class CommandParser { + static parseYAMLCommand(text) { + try { + // Extract command block between ^%$bridge and --- + const commandBlock = this.extractCommandBlock(text); + if (!commandBlock) { + throw new Error('No valid command block found'); + } + + // Parse YAML-like syntax + const parsed = this.parseKeyValuePairs(commandBlock); + + // Set defaults + parsed.url = parsed.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge'; + parsed.owner = parsed.owner || 'brrd'; + + return parsed; + } catch (error) { + throw new Error(`YAML parsing failed: ${error.message}`); + } + } + + static extractCommandBlock(text) { + const startMarker = '^%$bridge'; + const endMarker = '---'; + + const startIndex = text.indexOf(startMarker); + if (startIndex === -1) return null; + + const endIndex = text.indexOf(endMarker, startIndex + startMarker.length); + if (endIndex === -1) return null; + + return text.substring(startIndex + startMarker.length, endIndex).trim(); + } + + static parseKeyValuePairs(block) { + const lines = block.split('\n'); + const result = {}; + let currentKey = null; + let multiLineContent = null; + + for (const line of lines) { + const trimmed = line.trim(); + + if (!trimmed) continue; + + // Check for multi-line content marker + if (trimmed === '|' && currentKey === 'content') { + multiLineContent = []; + continue; + } + + // If we're collecting multi-line content + if (multiLineContent !== null) { + if (trimmed === '---') break; // End of command + multiLineContent.push(line); + continue; + } + + // Parse key: value pairs + const colonIndex = trimmed.indexOf(':'); + if (colonIndex !== -1) { + const key = trimmed.substring(0, colonIndex).trim(); + let value = trimmed.substring(colonIndex + 1).trim(); + + // Handle empty values that might be multi-line + if (value === '' || value === '|') { + currentKey = key; + if (value === '|') { + multiLineContent = []; + } + result[key] = ''; + } else { + result[key] = value; + currentKey = null; + } + } else if (currentKey && result[currentKey] === '') { + // Continue with previous key (simple multi-line) + result[currentKey] += '\n' + trimmed; + } + } + + // Join multi-line content + if (multiLineContent !== null && currentKey) { + result[currentKey] = multiLineContent.join('\n').trim(); + } + + return result; + } + + static validateStructure(parsed) { + const errors = []; + + // Check required fields + const action = parsed.action; + if (!action) { + errors.push('Missing required field: action'); + return { isValid: false, errors }; + } + + const requiredFields = REQUIRED_FIELDS[action]; + if (!requiredFields) { + errors.push(`Unknown action: ${action}`); + return { isValid: false, errors }; + } + + for (const field of requiredFields) { + if (!parsed[field] && parsed[field] !== '') { + errors.push(`Missing required field: ${field}`); + } + } + + // Validate field formats + 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 Manager Class + class ExecutionManager { + static async executeCommand(command, sourceElement) { + try { + // Pre-execution safety checks + if (!CONFIG.ENABLE_API) { + return this.mockExecution(command, sourceElement); + } + + // Show executing status + UIFeedback.replaceWithStatus(sourceElement, 'EXECUTING', { + action: command.action, + details: 'Making API request...' + }); + + // API call with retry logic + const response = await this.makeAPICallWithRetry(command); + return this.handleSuccess(response, command, sourceElement); + + } catch (error) { + return this.handleError(error, command, sourceElement); + } + } + + static async makeAPICallWithRetry(command, attempt = 0) { + try { + return await this.makeAPICall(command); + } catch (error) { + if (attempt < CONFIG.MAX_RETRIES) { + await this.delay(1000 * (attempt + 1)); + return this.makeAPICallWithRetry(command, attempt + 1); + } + throw error; + } + } + + static makeAPICall(command) { + return new Promise((resolve, reject) => { + GM_xmlhttpRequest({ + method: 'POST', + url: command.url, + headers: { + 'X-Bridge-Key': 'mango-rocket-82', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(command), + timeout: 30000, + onload: (response) => { + if (response.status >= 200 && response.status < 300) { + resolve(response); + } else { + reject(new Error(`API Error ${response.status}: ${response.statusText}`)); + } + }, + onerror: (error) => { + reject(new Error(`Network error: ${error}`)); + }, + ontimeout: () => { + reject(new Error('API request timeout')); + } + }); + }); + } + + static async mockExecution(command, sourceElement) { + await this.delay(1000); // Simulate API delay + + const mockResponse = { + status: 200, + responseText: JSON.stringify({ + success: true, + message: `Mock execution completed for ${command.action}`, + data: { command: command.action, repo: command.repo } + }) + }; + + return this.handleSuccess(mockResponse, command, sourceElement, true); + } + + static handleSuccess(response, command, sourceElement, isMock = false) { + const responseData = JSON.parse(response.responseText); + const templateType = isMock ? 'MOCK' : 'SUCCESS'; + + UIFeedback.replaceWithStatus(sourceElement, templateType, { + action: command.action, + details: responseData.message || 'Operation completed successfully' + }); + + return { + success: true, + data: responseData, + isMock + }; + } + + static handleError(error, command, sourceElement) { + UIFeedback.replaceWithStatus(sourceElement, 'ERROR', { + action: command.action, + details: error.message + }); + + return { + success: false, + error: error.message + }; + } + + static delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + } + + // UI Feedback System + class UIFeedback { + static replaceWithStatus(sourceElement, templateType, data) { + const statusElement = this.createStatusElement(templateType, data); + this.replaceElement(sourceElement, statusElement); + } + + static createStatusElement(templateType, data) { + const template = STATUS_TEMPLATES[templateType]; + const message = template + .replace('{action}', data.action) + .replace('{details}', data.details); + + const statusElement = document.createElement('div'); + statusElement.className = 'ai-repo-commander-status'; + statusElement.textContent = message; + statusElement.style.cssText = ` + padding: 8px 12px; + margin: 5px 0; + border-radius: 4px; + border-left: 4px solid ${this.colorCodeStatus(templateType)}; + background-color: rgba(255, 255, 255, 0.1); + font-family: monospace; + font-size: 14px; + white-space: pre-wrap; + word-wrap: break-word; + `; + + return statusElement; + } + + static replaceElement(oldElement, newElement) { + if (oldElement && oldElement.parentNode) { + oldElement.parentNode.replaceChild(newElement, oldElement); + } + } + + static colorCodeStatus(type) { + const colors = { + 'SUCCESS': '#10B981', // Green + 'ERROR': '#EF4444', // Red + 'VALIDATION_ERROR': '#F59E0B', // Yellow + 'EXECUTING': '#3B82F6', // Blue + 'MOCK': '#8B5CF6' // Purple + }; + return colors[type] || '#6B7280'; // Default gray + } + } + + // Test command generator + const TEST_COMMANDS = { + validUpdate: `^%$bridge +action: update_file +repo: test-repo +path: TEST.md +content: | + Test content + Multiple lines +---`, + + invalidCommand: `^%$bridge +action: update_file +repo: test-repo +---`, + + getFile: `^%$bridge +action: get_file +repo: test-repo +path: README.md +---` + }; + + // Initialize the system + let commandMonitor; + + function initializeRepoCommander() { + if (!commandMonitor) { + commandMonitor = new CommandMonitor(); + + // Expose for debugging + window.AI_REPO_COMMANDER = { + monitor: commandMonitor, + config: CONFIG, + test: TEST_COMMANDS, + version: CONFIG.VERSION + }; + + console.log('AI Repo Commander fully initialized'); + console.log('API Enabled:', CONFIG.ENABLE_API); + console.log('Test commands available in window.AI_REPO_COMMANDER.test'); + } + } + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeRepoCommander); + } else { + initializeRepoCommander(); + } + +})(); \ No newline at end of file