Update src/ai-repo-commander.user.js

This commit is contained in:
rob 2025-10-06 18:12:23 +00:00
parent 76c40fc87c
commit 945c7eca57
1 changed files with 599 additions and 39 deletions

View File

@ -1,40 +1,600 @@
// ==UserScript== // ==UserScript==
// @name AI Repo Commander // @name AI Repo Commander
// @namespace http://violentmonkey.com/ // @namespace http://tampermonkey.net/
// @version 1.0.0 // @version 1.0.0
// @description Enable AI assistants to interact with git repositories safely // @description Enable AI assistants to securely interact with git repositories via YAML commands
// @author rob // @author Your Name
// @match https://chat.openai.com/* // @match https://chat.openai.com/*
// @match https://claude.ai/* // @match https://claude.ai/*
// @match https://gemini.google.com/* // @match https://gemini.google.com/*
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @grant GM_notification // @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript== // ==/UserScript==
/*
* AI Repo Commander - Main Implementation
* Safety-first browser extension for AI-to-repo workflows
*/
(function() { (function() {
'use strict'; 'use strict';
// Master configuration - SAFETY FIRST // Configuration - MUST be manually enabled for production
const CONFIG = { const CONFIG = {
ENABLE_API: false, // MUST be manually enabled for production use ENABLE_API: false, // Master kill switch
DEBUG_MODE: true, // Development logging DEBUG_MODE: true, // Development logging
DEBOUNCE_DELAY: 5000, // 5-second bot typing protection DEBOUNCE_DELAY: 5000, // 5-second bot typing protection
MAX_RETRIES: 2, // API retry attempts MAX_RETRIES: 2, // API retry attempts
VERSION: '1.0.0' VERSION: '1.0.0'
}; };
console.log(`🚀 AI Repo Commander v${CONFIG.VERSION} loaded!`); // Platform-specific DOM selectors
console.log(`🔒 API Enabled: ${CONFIG.ENABLE_API}`); const PLATFORM_SELECTORS = {
console.log(`🐛 Debug Mode: ${CONFIG.DEBUG_MODE}`); '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'
}
};
// TODO: Implementation will go here // Required fields matrix
// This is the main file structure 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();
}
})(); })();