Compare commits
No commits in common. "205c2561feefa9a8a54c86b4e90ecbd520e512e6" and "ca94e0328978d031314e2271432bfa1e0baf5489" have entirely different histories.
205c2561fe
...
ca94e03289
File diff suppressed because it is too large
Load Diff
|
|
@ -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==
|
|
||||||
|
|
@ -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==
|
|
||||||
|
|
@ -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==
|
|
||||||
|
|
@ -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==
|
|
||||||
159
src/main.js
159
src/main.js
|
|
@ -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==
|
|
||||||
|
|
@ -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==
|
|
||||||
|
|
@ -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==
|
|
||||||
Loading…
Reference in New Issue