Update src/ai-repo-commander.user.js
Core Functionality Preserved: SKIP_AI_MESSAGES: false by default ✅ AI can execute commands it generates ✅ Your main use case works perfectly ✅ Safety Features Available: Optional AI message filtering (SKIP_AI_MESSAGES: true) ✅ Code block detection (always on) ✅ Content deduplication ✅ Memory cleanup ✅ Technical Improvements: Delay consistency (1000ms everywhere) ✅ Commit message included in mock response ✅
This commit is contained in:
parent
672d4d4475
commit
df4c18c3d5
|
|
@ -1,9 +1,10 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name AI Repo Commander
|
// @name AI Repo Commander
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 1.0.2
|
// @version 1.1.2
|
||||||
// @description Enable AI assistants to securely interact with git repositories via YAML commands, with safety-first execution and clear inline feedback.
|
// @description Safely execute ^%$bridge YAML commands in chat UIs with dedupe, debounce, and clear feedback — minimal and focused.
|
||||||
// @author Your Name
|
// @author Your Name
|
||||||
|
// @match https://chat.openai.com/*
|
||||||
// @match https://chatgpt.com/*
|
// @match https://chatgpt.com/*
|
||||||
// @match https://claude.ai/*
|
// @match https://claude.ai/*
|
||||||
// @match https://gemini.google.com/*
|
// @match https://gemini.google.com/*
|
||||||
|
|
@ -15,35 +16,32 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Configuration - MUST be manually enabled for production
|
// ---------------------- Config ----------------------
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
ENABLE_API: false, // Master kill switch
|
ENABLE_API: false, // Master kill switch
|
||||||
DEBUG_MODE: true, // Development logging
|
DEBUG_MODE: true, // Console logs
|
||||||
DEBOUNCE_DELAY: 5000, // 5-second bot typing protection
|
DEBOUNCE_DELAY: 5000, // Bot typing protection
|
||||||
MAX_RETRIES: 2, // API retry attempts (2 retries => up to 3 total attempts)
|
MAX_RETRIES: 2, // Retry attempts (=> up to MAX_RETRIES+1 total tries)
|
||||||
VERSION: '1.0.2'
|
VERSION: '1.1.2',
|
||||||
|
|
||||||
|
// Lean dedupe + cleanup (keep it simple)
|
||||||
|
SEEN_TTL_MS: 60000, // Deduplicate identical blocks for 60s
|
||||||
|
CLEANUP_AFTER_MS: 30000, // Drop COMPLETE/ERROR entries after 30s
|
||||||
|
CLEANUP_INTERVAL_MS: 60000, // Sweep cadence
|
||||||
|
|
||||||
|
// Optional safety: when true, only user-authored messages are processed
|
||||||
|
SKIP_AI_MESSAGES: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Platform-specific DOM selectors
|
// ---------------------- Platform selectors ----------------------
|
||||||
const PLATFORM_SELECTORS = {
|
const PLATFORM_SELECTORS = {
|
||||||
'chat.openai.com': {
|
'chat.openai.com': { messages: '[class*="message"]', input: '#prompt-textarea', content: '[class*="markdown"]' },
|
||||||
messages: '[class*="message"]',
|
'chatgpt.com': { messages: '[class*="message"]', input: '#prompt-textarea', content: '[class*="markdown"]' },
|
||||||
input: '#prompt-textarea',
|
'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' },
|
||||||
content: '[class*="markdown"]'
|
'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' }
|
||||||
},
|
|
||||||
'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
|
// ---------------------- Command requirements ----------------------
|
||||||
const REQUIRED_FIELDS = {
|
const REQUIRED_FIELDS = {
|
||||||
'update_file': ['action', 'repo', 'path', 'content'],
|
'update_file': ['action', 'repo', 'path', 'content'],
|
||||||
'get_file': ['action', 'repo', 'path'],
|
'get_file': ['action', 'repo', 'path'],
|
||||||
|
|
@ -53,18 +51,16 @@
|
||||||
'list_files': ['action', 'repo', 'path']
|
'list_files': ['action', 'repo', 'path']
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enhanced field validators (minimal safety)
|
|
||||||
const FIELD_VALIDATORS = {
|
const FIELD_VALIDATORS = {
|
||||||
// allow "owner/repo" or just "repo"
|
// allow "owner/repo" or just "repo"
|
||||||
'repo': (value) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(value),
|
'repo': (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v),
|
||||||
// basic traversal guard: no absolute paths, no backslashes, no ".."
|
// minimal traversal guard: no absolute paths, no backslashes, no ".."
|
||||||
'path': (value) => !value.includes('..') && !value.startsWith('/') && !value.includes('\\'),
|
'path': (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
|
||||||
'action': (value) => Object.keys(REQUIRED_FIELDS).includes(value),
|
'action': (v) => Object.keys(REQUIRED_FIELDS).includes(v),
|
||||||
'owner': (value) => !value || /^[\w\-]+$/.test(value),
|
'owner': (v) => !v || /^[\w\-]+$/.test(v),
|
||||||
'url': (value) => !value || /^https?:\/\/.+\..+/.test(value)
|
'url': (v) => !v || /^https?:\/\/.+\..+/.test(v)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Status message templates
|
|
||||||
const STATUS_TEMPLATES = {
|
const STATUS_TEMPLATES = {
|
||||||
SUCCESS: '[{action}: Success] {details}',
|
SUCCESS: '[{action}: Success] {details}',
|
||||||
ERROR: '[{action}: Error] {details}',
|
ERROR: '[{action}: Error] {details}',
|
||||||
|
|
@ -73,7 +69,6 @@
|
||||||
MOCK: '[{action}: Mock] {details}'
|
MOCK: '[{action}: Mock] {details}'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Command states
|
|
||||||
const COMMAND_STATES = {
|
const COMMAND_STATES = {
|
||||||
DETECTED: 'detected',
|
DETECTED: 'detected',
|
||||||
PARSING: 'parsing',
|
PARSING: 'parsing',
|
||||||
|
|
@ -84,7 +79,7 @@
|
||||||
ERROR: 'error'
|
ERROR: 'error'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ephemeral secret (memory only; prompted when API enabled)
|
// ---------------------- Ephemeral key ----------------------
|
||||||
let BRIDGE_KEY = null;
|
let BRIDGE_KEY = null;
|
||||||
function requireBridgeKeyIfNeeded() {
|
function requireBridgeKeyIfNeeded() {
|
||||||
if (CONFIG.ENABLE_API && !BRIDGE_KEY) {
|
if (CONFIG.ENABLE_API && !BRIDGE_KEY) {
|
||||||
|
|
@ -94,10 +89,233 @@
|
||||||
return BRIDGE_KEY;
|
return BRIDGE_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core Monitor Class
|
// ---------------------- Dedupe store (hash -> timestamp) ----------------------
|
||||||
|
const SEEN_MAP = new Map();
|
||||||
|
function hashBlock(str) {
|
||||||
|
// djb2/xor variant -> base36
|
||||||
|
let h = 5381;
|
||||||
|
for (let i = 0; i < str.length; i++) h = ((h << 5) + h) ^ str.charCodeAt(i);
|
||||||
|
return (h >>> 0).toString(36);
|
||||||
|
}
|
||||||
|
function alreadySeenBlock(blockText) {
|
||||||
|
const now = Date.now();
|
||||||
|
const key = hashBlock(blockText);
|
||||||
|
const ts = SEEN_MAP.get(key);
|
||||||
|
if (ts && (now - ts) < CONFIG.SEEN_TTL_MS) return true;
|
||||||
|
SEEN_MAP.set(key, now);
|
||||||
|
// prune occasionally
|
||||||
|
for (const [k, t] of SEEN_MAP) if ((now - t) >= CONFIG.SEEN_TTL_MS) SEEN_MAP.delete(k);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- Parser ----------------------
|
||||||
|
class CommandParser {
|
||||||
|
static parseYAMLCommand(text) {
|
||||||
|
const block = this.extractCommandBlock(text);
|
||||||
|
if (!block) throw new Error('No valid command block found');
|
||||||
|
const parsed = this.parseKeyValuePairs(block);
|
||||||
|
|
||||||
|
// defaults
|
||||||
|
parsed.url = parsed.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge';
|
||||||
|
parsed.owner = parsed.owner || 'brrd';
|
||||||
|
|
||||||
|
// 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 extractCommandBlock(text) {
|
||||||
|
// require ^%$bridge ... --- (tolerate trailing spaces and EOF)
|
||||||
|
const m = text.match(/^\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m);
|
||||||
|
return m ? m[1].trimEnd() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseKeyValuePairs(block) {
|
||||||
|
const lines = block.split('\n');
|
||||||
|
const result = {};
|
||||||
|
let currentKey = null;
|
||||||
|
let collecting = false;
|
||||||
|
let buf = [];
|
||||||
|
const TOP = ['action','repo','path','content','owner','url','commit_message','branch','ref'];
|
||||||
|
|
||||||
|
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);
|
||||||
|
const isTopKey = looksKey && unindented && TOP.some(k => line.startsWith(k + ':'));
|
||||||
|
if (isTopKey) {
|
||||||
|
result[currentKey] = buf.join('\n').trimEnd();
|
||||||
|
collecting = false; buf = [];
|
||||||
|
// fallthrough to parse this line as a new key
|
||||||
|
} 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 = [];
|
||||||
|
const action = parsed.action;
|
||||||
|
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 {
|
||||||
|
// FIX: synthesize commit_message first so mock and real behave the same
|
||||||
|
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)); // 1s, 2s, ...
|
||||||
|
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: 30000,
|
||||||
|
onload: (response) => (response.status >= 200 && response.status < 300)
|
||||||
|
? resolve(response)
|
||||||
|
: 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); // consistency
|
||||||
|
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 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'
|
||||||
|
});
|
||||||
|
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)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- Monitor ----------------------
|
||||||
class CommandMonitor {
|
class CommandMonitor {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.trackedMessages = new Map(); // id -> { element, originalText, state, ... }
|
this.trackedMessages = new Map(); // id -> { element, originalText, state, lastUpdate }
|
||||||
this.observer = null;
|
this.observer = null;
|
||||||
this.currentPlatform = null;
|
this.currentPlatform = null;
|
||||||
this.initialize();
|
this.initialize();
|
||||||
|
|
@ -111,85 +329,112 @@
|
||||||
if (CONFIG.ENABLE_API) {
|
if (CONFIG.ENABLE_API) {
|
||||||
console.warn('[AI Repo Commander] API is enabled — you will be prompted for your bridge key on first command.');
|
console.warn('[AI Repo Commander] API is enabled — you will be prompted for your bridge key on first command.');
|
||||||
}
|
}
|
||||||
|
setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
detectPlatform() {
|
detectPlatform() {
|
||||||
const hostname = window.location.hostname;
|
const host = window.location.hostname;
|
||||||
this.currentPlatform = PLATFORM_SELECTORS[hostname] || PLATFORM_SELECTORS['chat.openai.com'];
|
this.currentPlatform = PLATFORM_SELECTORS[host] || PLATFORM_SELECTORS['chat.openai.com'];
|
||||||
}
|
}
|
||||||
|
|
||||||
startObservation() {
|
startObservation() {
|
||||||
this.observer = new MutationObserver((mutations) => {
|
this.observer = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((m) => {
|
||||||
mutation.addedNodes.forEach((node) => {
|
m.addedNodes.forEach((node) => {
|
||||||
if (node.nodeType === 1) { // Element
|
if (node.nodeType === 1) this.scanNode(node);
|
||||||
this.scanNode(node);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.observer.observe(document.body, { childList: true, subtree: true });
|
this.observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
// Initial scan
|
|
||||||
this.scanExistingMessages();
|
this.scanExistingMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
scanNode(node) {
|
scanNode(node) {
|
||||||
if (node.querySelector && node.querySelector(this.currentPlatform.messages)) {
|
if (node.querySelector && node.querySelector(this.currentPlatform.messages)) this.scanMessages();
|
||||||
this.scanMessages();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scanExistingMessages() {
|
scanExistingMessages() { setTimeout(() => this.scanMessages(), 1000); }
|
||||||
setTimeout(() => this.scanMessages(), 1000);
|
|
||||||
|
// Optional safety: when SKIP_AI_MESSAGES=true, only process user-authored messages
|
||||||
|
isUserMessage(element) {
|
||||||
|
if (!CONFIG.SKIP_AI_MESSAGES) return true; // default: process both user + assistant
|
||||||
|
const host = window.location.hostname;
|
||||||
|
if (host === 'chat.openai.com' || host === 'chatgpt.com') {
|
||||||
|
return !!element.closest?.('[data-message-author-role="user"]');
|
||||||
|
}
|
||||||
|
// For unknown platforms, be permissive (treat as user)
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
scanMessages() {
|
|
||||||
const messages = document.querySelectorAll(this.currentPlatform.messages);
|
|
||||||
messages.forEach((messageElement) => {
|
|
||||||
const messageId = this.getMessageId(messageElement);
|
|
||||||
if (this.trackedMessages.has(messageId)) return;
|
|
||||||
|
|
||||||
const text = this.extractText(messageElement);
|
|
||||||
if (text && text.includes('^%$bridge')) {
|
|
||||||
this.trackMessage(messageElement, text, messageId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stable id: persist a data attribute to avoid duplicate processing on rescans
|
|
||||||
getMessageId(element) {
|
getMessageId(element) {
|
||||||
if (element.dataset && element.dataset.aiRcId) return element.dataset.aiRcId;
|
if (element.dataset && element.dataset.aiRcId) return element.dataset.aiRcId;
|
||||||
|
// prefer DOM id; otherwise add a tiny salt to reduce collisions
|
||||||
const id = element.id || `${element.className}-${element.childElementCount}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
const id = element.id || `${element.className}-${element.childElementCount}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
||||||
if (element.dataset) element.dataset.aiRcId = id;
|
if (element.dataset) element.dataset.aiRcId = id;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
extractText(element) {
|
extractText(element) {
|
||||||
const contentElement = element.querySelector(this.currentPlatform.content);
|
const c = element.querySelector(this.currentPlatform.content);
|
||||||
return contentElement ? contentElement.textContent : element.textContent;
|
return (c ? c.textContent : element.textContent) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always ignore commands shown inside code/pre blocks so you can discuss examples safely
|
||||||
|
hasBridgeInCodeBlock(element) {
|
||||||
|
const nodes = element.querySelectorAll('pre, code');
|
||||||
|
for (const el of nodes) {
|
||||||
|
if ((el.textContent || '').includes('^%$bridge')) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scanMessages() {
|
||||||
|
const messages = document.querySelectorAll(this.currentPlatform.messages);
|
||||||
|
messages.forEach((el) => {
|
||||||
|
if (!this.isUserMessage(el)) return; // optional safety (disabled by default)
|
||||||
|
|
||||||
|
const id = this.getMessageId(el);
|
||||||
|
if (this.trackedMessages.has(id)) return;
|
||||||
|
|
||||||
|
if (this.hasBridgeInCodeBlock(el)) return; // discussing examples → ignore
|
||||||
|
|
||||||
|
const text = this.extractText(el);
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
// Require a full ^%$bridge ... --- block to avoid false positives
|
||||||
|
const m = text.match(/^\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m);
|
||||||
|
if (!m) return;
|
||||||
|
|
||||||
|
const wholeBlock = m[0];
|
||||||
|
if (alreadySeenBlock(wholeBlock)) return; // dedupe across multi-wrapper renders
|
||||||
|
|
||||||
|
this.trackMessage(el, text, id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
trackMessage(element, text, messageId) {
|
trackMessage(element, text, messageId) {
|
||||||
this.log('New command detected:', { messageId, text: text.substring(0, 100) });
|
this.log('New command detected:', { messageId, text: text.substring(0, 120) });
|
||||||
this.trackedMessages.set(messageId, {
|
this.trackedMessages.set(messageId, {
|
||||||
element,
|
element, originalText: text, state: COMMAND_STATES.DETECTED, startTime: Date.now(), lastUpdate: Date.now()
|
||||||
originalText: text,
|
|
||||||
state: COMMAND_STATES.DETECTED,
|
|
||||||
startTime: Date.now()
|
|
||||||
});
|
});
|
||||||
this.updateState(messageId, COMMAND_STATES.PARSING);
|
this.updateState(messageId, COMMAND_STATES.PARSING);
|
||||||
this.processCommand(messageId);
|
this.processCommand(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState(messageId, state) {
|
updateState(messageId, state) {
|
||||||
const message = this.trackedMessages.get(messageId);
|
const msg = this.trackedMessages.get(messageId);
|
||||||
if (message) {
|
if (!msg) return;
|
||||||
message.state = state;
|
msg.state = state;
|
||||||
message.lastUpdate = Date.now();
|
msg.lastUpdate = Date.now();
|
||||||
this.trackedMessages.set(messageId, message);
|
this.trackedMessages.set(messageId, msg);
|
||||||
this.log(`Message ${messageId} state updated to: ${state}`);
|
this.log(`Message ${messageId} state updated to: ${state}`);
|
||||||
|
|
||||||
|
// Terminal cleanup after grace period (kept shortly for debugging)
|
||||||
|
if (state === COMMAND_STATES.COMPLETE || state === COMMAND_STATES.ERROR) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.trackedMessages.delete(messageId);
|
||||||
|
this.log(`Cleaned up message ${messageId}`);
|
||||||
|
}, CONFIG.CLEANUP_AFTER_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,37 +443,23 @@
|
||||||
const message = this.trackedMessages.get(messageId);
|
const message = this.trackedMessages.get(messageId);
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
|
|
||||||
// Step 1: Parse command
|
const parsed = CommandParser.parseYAMLCommand(message.originalText);
|
||||||
const parsedCommand = CommandParser.parseYAMLCommand(message.originalText);
|
|
||||||
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
||||||
|
|
||||||
// Step 2: Validate command
|
const validation = CommandParser.validateStructure(parsed);
|
||||||
const validation = CommandParser.validateStructure(parsedCommand);
|
if (!validation.isValid) throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
||||||
if (!validation.isValid) {
|
|
||||||
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Debounce with content change detection (handles streaming edits)
|
|
||||||
this.updateState(messageId, COMMAND_STATES.DEBOUNCING);
|
this.updateState(messageId, COMMAND_STATES.DEBOUNCING);
|
||||||
const initialText = message.originalText;
|
const before = message.originalText;
|
||||||
await this.debounce();
|
await this.debounce();
|
||||||
const currentText = this.extractText(message.element);
|
const after = this.extractText(message.element);
|
||||||
if (currentText && currentText !== initialText) {
|
if (after && after !== before) {
|
||||||
this.log('Content changed during debounce; restarting debounce.');
|
this.log('Content changed during debounce; restarting debounce.');
|
||||||
await this.debounce();
|
await this.debounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Optional commit message synthesis
|
|
||||||
if ((parsedCommand.action === 'update_file' || parsedCommand.action === 'create_file') &&
|
|
||||||
!parsedCommand.commit_message) {
|
|
||||||
parsedCommand.commit_message = `AI Repo Commander: ${parsedCommand.path} (${new Date().toISOString()})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Execute command
|
|
||||||
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
||||||
await ExecutionManager.executeCommand(parsedCommand, message.element);
|
await ExecutionManager.executeCommand(parsed, message.element);
|
||||||
|
|
||||||
// Step 6: Complete
|
|
||||||
this.updateState(messageId, COMMAND_STATES.COMPLETE);
|
this.updateState(messageId, COMMAND_STATES.COMPLETE);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -236,16 +467,21 @@
|
||||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||||
const message = this.trackedMessages.get(messageId);
|
const message = this.trackedMessages.get(messageId);
|
||||||
if (message) {
|
if (message) {
|
||||||
UIFeedback.appendStatus(message.element, 'ERROR', {
|
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message });
|
||||||
action: 'Command',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debounce() {
|
debounce() { return new Promise((r) => setTimeout(r, CONFIG.DEBOUNCE_DELAY)); }
|
||||||
return new Promise(resolve => setTimeout(resolve, CONFIG.DEBOUNCE_DELAY));
|
|
||||||
|
cleanupProcessedCommands() {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [id, msg] of this.trackedMessages.entries()) {
|
||||||
|
if ((msg.state === COMMAND_STATES.COMPLETE || msg.state === COMMAND_STATES.ERROR) &&
|
||||||
|
now - (msg.lastUpdate || now) > CONFIG.CLEANUP_AFTER_MS) {
|
||||||
|
this.trackedMessages.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAllProcessing() {
|
stopAllProcessing() {
|
||||||
|
|
@ -258,293 +494,14 @@
|
||||||
CONFIG.ENABLE_API = false;
|
CONFIG.ENABLE_API = false;
|
||||||
this.stopAllProcessing();
|
this.stopAllProcessing();
|
||||||
this.log('EMERGENCY STOP ACTIVATED');
|
this.log('EMERGENCY STOP ACTIVATED');
|
||||||
GM_notification({
|
GM_notification({ text: 'AI Repo Commander Emergency Stop Activated', title: 'Safety System', timeout: 5000 });
|
||||||
text: 'AI Repo Commander Emergency Stop Activated',
|
|
||||||
title: 'Safety System',
|
|
||||||
timeout: 5000
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
log(...args) {
|
log(...args) { if (CONFIG.DEBUG_MODE) console.log('[AI Repo Commander]', ...args); }
|
||||||
if (CONFIG.DEBUG_MODE) console.log('[AI Repo Commander]', ...args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command Parser Class (sturdier block extraction + simple content: | handling)
|
// ---------------------- Test commands (unchanged) ----------------------
|
||||||
class CommandParser {
|
|
||||||
static parseYAMLCommand(text) {
|
|
||||||
try {
|
|
||||||
const commandBlock = this.extractCommandBlock(text);
|
|
||||||
if (!commandBlock) throw new Error('No valid command block found');
|
|
||||||
const parsed = this.parseKeyValuePairs(commandBlock);
|
|
||||||
|
|
||||||
// Defaults
|
|
||||||
parsed.url = parsed.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge';
|
|
||||||
parsed.owner = parsed.owner || 'brrd';
|
|
||||||
|
|
||||||
// Expand owner/repo shorthand here (not in validate)
|
|
||||||
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;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`YAML parsing failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static extractCommandBlock(text) {
|
|
||||||
// Find ^%$bridge … up to a line with --- (allow trailing spaces) then newline OR end of string
|
|
||||||
const m = text.match(/^\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m);
|
|
||||||
return m ? m[1].trimEnd() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseKeyValuePairs(block) {
|
|
||||||
const lines = block.split('\n');
|
|
||||||
const result = {};
|
|
||||||
let currentKey = null;
|
|
||||||
let collecting = false;
|
|
||||||
let buf = [];
|
|
||||||
const TOP_LEVEL_KEYS = ['action','repo','path','content','owner','url','commit_message','branch','ref'];
|
|
||||||
|
|
||||||
for (const raw of lines) {
|
|
||||||
const line = raw.replace(/\r$/, '');
|
|
||||||
|
|
||||||
if (collecting) {
|
|
||||||
// Only stop if UNINDENTED and a KNOWN top-level key
|
|
||||||
const looksKey = /^[A-Za-z_][\w\-]*\s*:/.test(line);
|
|
||||||
const unindented = !/^[ \t]/.test(line);
|
|
||||||
const isTopKey = looksKey && unindented &&
|
|
||||||
TOP_LEVEL_KEYS.some(k => line.startsWith(k + ':'));
|
|
||||||
if (isTopKey) {
|
|
||||||
result[currentKey] = buf.join('\n').trimEnd();
|
|
||||||
collecting = false;
|
|
||||||
buf = [];
|
|
||||||
// fall through to parse this line as a new key
|
|
||||||
} 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 = [];
|
|
||||||
|
|
||||||
// Required fields per action
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field format validators (includes path traversal guard)
|
|
||||||
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 (append status, clearer retry errors, ephemeral key)
|
|
||||||
class ExecutionManager {
|
|
||||||
static async executeCommand(command, sourceElement) {
|
|
||||||
try {
|
|
||||||
if (!CONFIG.ENABLE_API) {
|
|
||||||
return this.mockExecution(command, sourceElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
UIFeedback.appendStatus(sourceElement, 'EXECUTING', {
|
|
||||||
action: command.action,
|
|
||||||
details: 'Making API request...'
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
|
||||||
requireBridgeKeyIfNeeded();
|
|
||||||
return await this.makeAPICall(command);
|
|
||||||
} catch (error) {
|
|
||||||
if (attempt < CONFIG.MAX_RETRIES) {
|
|
||||||
await this.delay(1000 * (attempt + 1)); // simple backoff: 1s, 2s, ...
|
|
||||||
return this.makeAPICallWithRetry(command, attempt + 1);
|
|
||||||
}
|
|
||||||
const totalAttempts = attempt + 1; // attempts actually made
|
|
||||||
throw new Error(`${error.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: 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, path: command.path }
|
|
||||||
})
|
|
||||||
};
|
|
||||||
return this.handleSuccess(mockResponse, command, sourceElement, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static handleSuccess(response, command, sourceElement, isMock = false) {
|
|
||||||
let responseData;
|
|
||||||
try {
|
|
||||||
responseData = JSON.parse(response.responseText || '{}');
|
|
||||||
} catch {
|
|
||||||
responseData = { message: 'Operation completed (no JSON body)' };
|
|
||||||
}
|
|
||||||
const templateType = isMock ? 'MOCK' : 'SUCCESS';
|
|
||||||
|
|
||||||
UIFeedback.appendStatus(sourceElement, templateType, {
|
|
||||||
action: command.action,
|
|
||||||
details: responseData.message || 'Operation completed successfully'
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, data: responseData, 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(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI Feedback System (APPENDS instead of replacing)
|
|
||||||
class UIFeedback {
|
|
||||||
static appendStatus(sourceElement, templateType, data) {
|
|
||||||
const statusElement = this.createStatusElement(templateType, data);
|
|
||||||
const existingStatus = sourceElement.querySelector('.ai-repo-commander-status');
|
|
||||||
if (existingStatus) existingStatus.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 statusElement = document.createElement('div');
|
|
||||||
statusElement.className = 'ai-repo-commander-status';
|
|
||||||
statusElement.textContent = message;
|
|
||||||
statusElement.style.cssText = `
|
|
||||||
padding: 8px 12px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 4px solid ${this.colorCodeStatus(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 statusElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (unchanged)
|
|
||||||
const TEST_COMMANDS = {
|
const TEST_COMMANDS = {
|
||||||
validUpdate: `^%$bridge
|
validUpdate: `^%$bridge
|
||||||
action: update_file
|
action: update_file
|
||||||
|
|
@ -568,19 +525,12 @@ path: README.md
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the system
|
// ---------------------- Init ----------------------
|
||||||
let commandMonitor;
|
let commandMonitor;
|
||||||
|
|
||||||
function initializeRepoCommander() {
|
function initializeRepoCommander() {
|
||||||
if (!commandMonitor) {
|
if (!commandMonitor) {
|
||||||
commandMonitor = new CommandMonitor();
|
commandMonitor = new CommandMonitor();
|
||||||
// Expose for debugging
|
window.AI_REPO_COMMANDER = { monitor: commandMonitor, config: CONFIG, test: TEST_COMMANDS, version: CONFIG.VERSION };
|
||||||
window.AI_REPO_COMMANDER = {
|
|
||||||
monitor: commandMonitor,
|
|
||||||
config: CONFIG,
|
|
||||||
test: TEST_COMMANDS,
|
|
||||||
version: CONFIG.VERSION
|
|
||||||
};
|
|
||||||
console.log('AI Repo Commander fully initialized');
|
console.log('AI Repo Commander fully initialized');
|
||||||
console.log('API Enabled:', CONFIG.ENABLE_API);
|
console.log('API Enabled:', CONFIG.ENABLE_API);
|
||||||
console.log('Test commands available in window.AI_REPO_COMMANDER.test');
|
console.log('Test commands available in window.AI_REPO_COMMANDER.test');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue