Compare commits

..

No commits in common. "205c2561feefa9a8a54c86b4e90ecbd520e512e6" and "ca94e0328978d031314e2271432bfa1e0baf5489" have entirely different histories.

8 changed files with 3330 additions and 609 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,112 +0,0 @@
// ==COMMAND EXECUTOR START==
/* global GM_xmlhttpRequest */
(function () {
class CommandExecutor {
static async execute(command, sourceElement, label = '') {
const log = window.AI_REPO_LOGGER;
const cfg = window.AI_REPO_CONFIG;
try {
if (['update_file', 'create_file'].includes(command.action) && !command.commit_message) {
command.commit_message = `AI Repo Commander: ${command.path} (${new Date().toISOString()})`;
}
if (!cfg.get('api.enabled')) {
log.info('Mock executing', { action: command.action, label });
await this.delay(300);
return this._success({ status: 200, responseText: JSON.stringify({ success: true, message: 'Mock execution completed' }) }, command, sourceElement, true, label);
}
log.info('Executing via API', { action: command.action, label });
const res = await this._api(command);
return this._success(res, command, sourceElement, false, label);
} catch (err) {
window.AI_REPO_LOGGER.error('Execution failed', { action: command.action, error: err.message });
return this._error(err, command, sourceElement, label);
}
}
static _api(command, attempt = 0) {
const cfg = window.AI_REPO_CONFIG;
const maxRetries = cfg.get('api.maxRetries') ?? 2;
const timeout = cfg.get('api.timeout') ?? 60000;
const bridgeKey = this._getBridgeKey();
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: command.url,
headers: { 'X-Bridge-Key': bridgeKey, 'Content-Type': 'application/json' },
data: JSON.stringify(command),
timeout,
onload: (r) => (r.status >= 200 && r.status < 300) ? resolve(r) : reject(new Error(`API Error ${r.status}: ${r.statusText}`)),
onerror: (e) => {
if (attempt < maxRetries) {
setTimeout(() => this._api(command, attempt + 1).then(resolve).catch(reject), 1000 * (attempt + 1));
} else reject(new Error(`Network error after ${attempt + 1} attempts: ${e?.error || 'unknown'}`));
},
ontimeout: () => reject(new Error(`API timeout after ${timeout}ms`))
});
});
}
static _getBridgeKey() {
const cfg = window.AI_REPO_CONFIG;
let key = cfg.get('api.bridgeKey');
if (!key) {
key = prompt('[AI Repo Commander] Enter your bridge key for this session:') || '';
if (!key) throw new Error('Bridge key required when API is enabled');
if (confirm('Save this bridge key to avoid future prompts?')) cfg.set('api.bridgeKey', key);
}
return key;
}
static _success(response, command, el, isMock = false, label = '') {
let data; try { data = JSON.parse(response.responseText || '{}'); } catch { data = { message: 'Operation completed' }; }
this._status(el, isMock ? 'MOCK' : 'SUCCESS', { action: command.action, details: data.message || 'Completed successfully', label });
if (command.action === 'get_file') this._handleGetFile(data, label);
if (command.action === 'list_files') this._handleListFiles(data, label);
return { success: true, data, isMock };
}
static _error(error, command, el, label = '') {
this._status(el, 'ERROR', { action: command.action, details: error.message, label });
return { success: false, error: error.message };
}
static _status(el, type, data) {
const div = document.createElement('div');
div.style.cssText = `
padding:8px 12px;margin:8px 0;border-radius:4px;
border-left:4px solid ${this._color(type)};
background:rgba(255,255,255,.05);font-family:monospace;font-size:13px;white-space:pre-wrap;
`;
div.textContent = `${data.label || data.action}${type}${data.details ? ': ' + data.details : ''}`;
el.appendChild(div);
}
static _color(t){ return ({SUCCESS:'#10B981', ERROR:'#EF4444', MOCK:'#8B5CF6'})[t] || '#6B7280'; }
static _handleGetFile(data, label) {
const content = data?.content?.data ?? data?.content ?? data?.result?.content?.data ?? data?.result?.content;
if (!content) return;
window.AI_REPO_RESPONSES = window.AI_REPO_RESPONSES || [];
window.AI_REPO_RESPONSES.push({ label, content });
}
static _handleListFiles(data, label) {
const files = data?.files ?? data?.result?.files;
if (!Array.isArray(files)) return;
const listing = '```text\n' + files.map(f => (typeof f === 'string' ? f : (f?.path || f?.name || JSON.stringify(f)))).join('\n') + '\n```';
window.AI_REPO_RESPONSES = window.AI_REPO_RESPONSES || [];
window.AI_REPO_RESPONSES.push({ label, content: listing });
}
static delay(ms) { return new Promise(r => setTimeout(r, ms)); }
}
window.AI_REPO_EXECUTOR = CommandExecutor;
})();
// ==COMMAND EXECUTOR END==

View File

@ -1,98 +0,0 @@
// ==COMMAND PARSER START==
(function () {
class CommandParser {
static REQUIRED = {
get_file: ['action', 'repo', 'path'],
update_file: ['action', 'repo', 'path', 'content'],
create_file: ['action', 'repo', 'path', 'content'],
create_repo: ['action', 'repo'],
create_branch:['action', 'repo', 'branch'],
create_pr: ['action', 'repo', 'title', 'head', 'base'],
list_files: ['action', 'repo', 'path']
};
static parse(text) {
const block = this.extractBlock(text);
if (!block) throw new Error('No complete @bridge@ command found (missing @end@)');
const parsed = this.parseKV(block);
this.applyDefaults(parsed);
return parsed;
}
static extractBlock(text) {
const m = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/m.exec(text);
return m?.[1]?.trim() || null;
}
// Simple YAML-like parser (supports "key: value" & "key: |" multiline)
static parseKV(block) {
const out = {};
const lines = block.split('\n');
let curKey = null, multi = false, buf = [];
const flush = () => { if (multi && curKey) out[curKey] = buf.join('\n').replace(/\s+$/,''); curKey = null; buf = []; multi = false; };
for (const raw of lines) {
const line = raw.replace(/\r$/, '');
if (multi) {
// End multiline if we see an unindented key pattern
if (/^[A-Za-z_][\w\-]*\s*:/.test(line) && !/^\s/.test(line)) {
flush();
} 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 === '|') {
curKey = key; multi = true; buf = [];
} else {
out[key] = value;
curKey = key;
}
} else if (multi) {
buf.push(line);
}
}
flush();
return out;
}
static applyDefaults(p) {
p.url = p.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge';
p.owner = p.owner || 'rob';
if (p.action === 'create_branch' && !p.source_branch) p.source_branch = 'main';
if (typeof p.repo === 'string' && p.repo.includes('/')) {
const [owner, repo] = p.repo.split('/', 2);
if (!p.owner) p.owner = owner;
p.repo = repo;
}
}
static validate(p) {
const errors = [];
// explicit example flag
if (p.example === true || String(p.example).toLowerCase() === 'true' || String(p.example).toLowerCase() === 'yes') {
return { isValid: true, errors: [], example: true };
}
const action = p.action;
if (!action) return { isValid: false, errors: ['Missing required field: action'] };
const req = this.REQUIRED[action];
if (!req) return { isValid: false, errors: [`Unknown action: ${action}`] };
for (const f of req) if (!(f in p) || p[f] === '') errors.push(`Missing required field: ${f}`);
return { isValid: errors.length === 0, errors };
}
}
window.AI_REPO_PARSER = CommandParser;
})();
// ==COMMAND PARSER END==

View File

@ -1,99 +0,0 @@
// ==CONFIG START==
(function () {
const STORAGE_KEYS = {
history: 'ai_repo_commander_executed',
cfg: 'ai_repo_commander_cfg',
panel: 'ai_repo_commander_panel_state'
};
const DEFAULT_CONFIG = {
meta: { version: '1.6.2' },
api: {
enabled: true,
timeout: 60000,
maxRetries: 2,
bridgeKey: ''
},
debug: {
enabled: true,
level: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace
maxLines: 400,
showPanel: true
},
execution: {
debounceDelay: 6500,
settleCheckMs: 1300,
settlePollMs: 250,
requireTerminator: true
},
queue: {
minDelayMs: 1500,
maxPerMinute: 15,
maxPerMessage: 5
},
ui: {
autoSubmit: true,
appendTrailingNewline: true,
postPasteDelayMs: 600,
showExecutedMarker: true,
processExisting: false // used by main.js
},
// Runtime state (not persisted)
runtime: {
paused: false
}
};
class ConfigManager {
constructor() { this.config = this.load(); }
load() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
if (!raw) return this.deepClone(DEFAULT_CONFIG);
const saved = JSON.parse(raw);
return this.mergeConfigs(DEFAULT_CONFIG, saved);
} catch {
return this.deepClone(DEFAULT_CONFIG);
}
}
save() {
try {
const { runtime, ...persistable } = this.config; // do not persist runtime
localStorage.setItem('ai_repo_commander_cfg', JSON.stringify(persistable));
} catch (e) { console.warn('Failed to save config:', e); }
}
get(keyPath) {
return keyPath.split('.').reduce((obj, key) => obj?.[key], this.config);
}
set(keyPath, value) {
const keys = keyPath.split('.');
const last = keys.pop();
const tgt = keys.reduce((o, k) => (o[k] = o[k] || {}), this.config);
tgt[last] = value;
this.save();
}
mergeConfigs(defaults, saved) {
const out = this.deepClone(defaults);
for (const k of Object.keys(saved)) {
if (k === 'runtime') continue; // never restore runtime
if (typeof out[k] === 'object' && !Array.isArray(out[k])) {
out[k] = { ...out[k], ...saved[k] };
} else {
out[k] = saved[k];
}
}
return out;
}
deepClone(o) { return JSON.parse(JSON.stringify(o)); }
}
window.AI_REPO_CONFIG = new ConfigManager();
window.AI_REPO_STORAGE_KEYS = STORAGE_KEYS;
})();
// ==CONFIG END==

View File

@ -1,57 +0,0 @@
// ==LOGGER START==
(function () {
class Logger {
constructor() {
this.config = window.AI_REPO_CONFIG;
this.buffer = [];
}
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, 'VERBOSE', msg, data); }
trace(msg, data) { this._log(5, 'TRACE', msg, data); }
_log(levelNum, levelName, msg, data) {
const enabled = !!this.config.get('debug.enabled');
const level = this.config.get('debug.level') ?? 0;
if (!enabled || levelNum > level) return;
const entry = {
timestamp: new Date().toISOString(),
level: levelName,
message: String(msg),
data: this._sanitize(data)
};
this.buffer.push(entry);
const maxLines = this.config.get('debug.maxLines') || 400;
if (this.buffer.length > maxLines) this.buffer.splice(0, this.buffer.length - maxLines);
const prefix = `[AI RC ${levelName}]`;
entry.data ? console.log(prefix, msg, entry.data) : console.log(prefix, msg);
}
_sanitize(data) {
if (!data) return null;
if (data instanceof HTMLElement) return `HTMLElement<${data.tagName}>`;
if (typeof data === 'string' && data.length > 200) return data.slice(0, 200) + '…';
if (typeof data === 'object') {
const out = {};
for (const [k, v] of Object.entries(data)) {
out[k] = v instanceof HTMLElement ? `HTMLElement<${v.tagName}>` :
(typeof v === 'string' && v.length > 200 ? v.slice(0, 200) + '…' : v);
}
return out;
}
return data;
}
getRecentLogs(n = 50) {
return this.buffer.slice(-n).map(e =>
`${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`
).join('\n');
}
}
window.AI_REPO_LOGGER = new Logger();
})();
// ==LOGGER END==

View File

@ -1,159 +0,0 @@
// ==MAIN START==
(function () {
'use strict';
if (!window.AI_REPO_CONFIG || !window.AI_REPO_LOGGER || !window.AI_REPO_HISTORY || !window.AI_REPO_PARSER || !window.AI_REPO_EXECUTOR) {
console.error('AI Repo Commander: Core modules not loaded');
return;
}
const logger = window.AI_REPO_LOGGER;
const config = window.AI_REPO_CONFIG;
const history = window.AI_REPO_HISTORY;
class AIRepoCommander {
constructor() {
this.isInitialized = false;
this.observer = null;
this.processed = new WeakSet();
this.messageSelectors = [
'[data-message-author-role="assistant"]',
'.chat-message:not([data-message-author-role="user"])',
'.message-content'
];
}
initialize() {
if (this.isInitialized) return;
logger.info('AI Repo Commander initializing', {
version: config.get('meta.version'),
debugLevel: config.get('debug.level'),
apiEnabled: config.get('api.enabled')
});
this.startObserver();
if (config.get('ui.processExisting')) this.scanExisting();
this.exposeAPI();
this.isInitialized = true;
logger.info('AI Repo Commander initialized');
}
startObserver() {
this.observer = new MutationObserver((mutations) => {
if (config.get('runtime.paused')) return;
for (const m of mutations) {
if (m.type !== 'childList') continue;
for (const n of m.addedNodes) {
if (n.nodeType !== 1) continue;
if (this.isAssistantMessage(n)) this.processMessage(n);
const inner = n.querySelectorAll?.(this.messageSelectors.join(',')) || [];
inner.forEach(el => this.isAssistantMessage(el) && this.processMessage(el));
}
}
});
this.observer.observe(document.body, { childList: true, subtree: true });
}
isAssistantMessage(el) {
return this.messageSelectors.some(sel => el.matches?.(sel));
}
processMessage(el) {
if (this.processed.has(el)) return;
const commands = this.extractCommands(el);
if (!commands.length) return;
this.processed.add(el);
commands.slice(0, config.get('queue.maxPerMessage')).forEach((cmdText, idx) => {
if (history.isProcessed(el, idx)) {
this.addRetryButton(el, cmdText, idx);
} else {
this.run(el, cmdText, idx);
}
});
}
extractCommands(el) {
const text = el.textContent || '';
const out = [];
const re = /@bridge@[\s\S]*?@end@/g;
let m;
while ((m = re.exec(text)) !== null) out.push(m[0]);
return out;
}
async run(el, commandText, index) {
try {
history.markProcessed(el, index);
const parsed = window.AI_REPO_PARSER.parse(commandText);
const validation = window.AI_REPO_PARSER.validate(parsed);
if (!validation.isValid) {
logger.error('Command validation failed', { errors: validation.errors, command: parsed.action });
this.addRetryButton(el, commandText, index);
return;
}
if (validation.example) {
logger.info('Skipping example command');
return;
}
await this.delay(config.get('execution.debounceDelay') || 0);
const label = `Command ${index + 1}`;
await window.AI_REPO_EXECUTOR.execute(parsed, el, label);
} catch (e) {
logger.error('Command execution failed', { error: e.message, commandIndex: index });
this.addRetryButton(el, commandText, index);
}
}
addRetryButton(el, commandText, idx) {
const btn = document.createElement('button');
btn.textContent = `Run Again #${idx + 1}`;
btn.style.cssText = `
padding:4px 8px;margin:4px;border:1px solid #374151;border-radius:4px;
background:#1f2937;color:#e5e7eb;cursor:pointer;
`;
btn.addEventListener('click', () => this.run(el, commandText, idx));
el.appendChild(btn);
}
scanExisting() {
const nodes = document.querySelectorAll(this.messageSelectors.join(','));
nodes.forEach(el => this.isAssistantMessage(el) && this.processMessage(el));
}
exposeAPI() {
window.AI_REPO_COMMANDER = {
version: config.get('meta.version'),
config,
logger,
history,
pause: () => { config.set('runtime.paused', true); logger.info('Paused'); },
resume: () => { config.set('runtime.paused', false); logger.info('Resumed'); },
clearHistory: () => { history.clear(); logger.info('History cleared'); }
};
}
delay(ms) { return new Promise(r => setTimeout(r, ms)); }
destroy() {
this.observer?.disconnect();
this.processed = new WeakSet();
this.isInitialized = false;
logger.info('AI Repo Commander destroyed');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.AI_REPO_MAIN = new AIRepoCommander();
window.AI_REPO_MAIN.initialize();
});
} else {
window.AI_REPO_MAIN = new AIRepoCommander();
window.AI_REPO_MAIN.initialize();
}
})();
// ==MAIN END==

View File

@ -1,64 +0,0 @@
// ==STORAGE START==
(function () {
class ConversationHistory {
constructor() {
this.conversationId = this._getConversationId();
this.key = `ai_rc:conv:${this.conversationId}:processed`;
this.cache = this._load();
this._cleanupExpired();
}
_getConversationId() {
const host = location.hostname.replace('chat.openai.com', 'chatgpt.com');
return `${host}:${location.pathname || '/'}`;
}
_load() {
try { return JSON.parse(localStorage.getItem(this.key) || '{}'); }
catch { return {}; }
}
_save() {
try { localStorage.setItem(this.key, JSON.stringify(this.cache)); }
catch (e) { window.AI_REPO_LOGGER?.warn('Failed to save history cache', { error: e.message }); }
}
isProcessed(el, commandIndex = 0) {
const fp = this._fingerprint(el, commandIndex);
return Object.prototype.hasOwnProperty.call(this.cache, fp);
}
markProcessed(el, commandIndex = 0) {
const fp = this._fingerprint(el, commandIndex);
this.cache[fp] = Date.now();
this._save();
if (window.AI_REPO_CONFIG.get('ui.showExecutedMarker')) this._mark(el);
}
_fingerprint(el, idx) {
const text = (el.textContent || '').slice(0, 1000);
const list = Array.from(document.querySelectorAll('[data-message-author-role], .chat-message, .message-content'));
const pos = list.indexOf(el);
return `conv:${this.conversationId}|pos:${pos}|idx:${idx}|hash:${this._hash(text)}`;
}
_hash(str) {
let h = 5381;
for (let i = 0; i < Math.min(str.length, 1000); i++) h = ((h << 5) + h) ^ str.charCodeAt(i);
return (h >>> 0).toString(36);
}
_mark(el) { try { el.style.borderLeft = '3px solid #10B981'; } catch {} }
_cleanupExpired() {
const ttl = 30 * 24 * 60 * 60 * 1000;
const now = Date.now();
let dirty = false;
for (const [k, ts] of Object.entries(this.cache)) {
if (!ts || now - ts > ttl) { delete this.cache[k]; dirty = true; }
}
if (dirty) this._save();
}
clear() { this.cache = {}; this._save(); }
}
window.AI_REPO_HISTORY = new ConversationHistory();
})();
// ==STORAGE END==

View File

@ -1,20 +0,0 @@
// ==UserScript==
// @name AI Repo Commander (Modular Refactor)
// @namespace http://tampermonkey.net/
// @version 2.0.0-mod
// @description Modularized: config, logger, storage, parser, executor, main
// @author You
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @match https://claude.ai/*
// @match https://gemini.google.com/*
// @grant GM_xmlhttpRequest
// @connect n8n.brrd.tech
// @connect *
// @require https://YOUR_HOST/config.js
// @require https://YOUR_HOST/logger.js
// @require https://YOUR_HOST/storage.js
// @require https://YOUR_HOST/command-parser.js
// @require https://YOUR_HOST/command-executor.js
// @require https://YOUR_HOST/main.js
// ==/UserScript==