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

Debug threshold clarified & correct

logLoop() logs at a visible level (INFO by default)

Cleanup summary logs at INFO

Console output is suppressed unless DEBUG_LEVEL ≥ 3 (verbose/trace)

Clipboard copy has a safe fallback

Pause button shows a clear visual state

Panel mount is resilient if document.body isn’t ready

Emergency STOP also clears the cleanup interval

Initial scan explicitly skipped when PROCESS_EXISTING: false

Plus the earlier low-risk hardening (require action:; pre-mark history)
This commit is contained in:
rob 2025-10-07 18:51:39 +00:00
parent e75a06a751
commit bd53e289cf
1 changed files with 331 additions and 85 deletions

View File

@ -1,8 +1,8 @@
// ==UserScript==
// @name AI Repo Commander
// @namespace http://tampermonkey.net/
// @version 1.3.0
// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe, robust paste, and optional auto-submit
// @version 1.3.2
// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe, robust paste, optional auto-submit, and a built-in debug console
// @author Your Name
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
@ -19,14 +19,18 @@
// ---------------------- Config ----------------------
const CONFIG = {
ENABLE_API: true, // Master kill switch
DEBUG_MODE: true, // Console logs
ENABLE_API: true, // Master kill switch (STOP API flips this to false)
DEBUG_MODE: true, // Global on/off for debug logging
DEBUG_LEVEL: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace
DEBUG_WATCH_MS: 120000, // Only log tight loop spam for the first 2 minutes
DEBUG_MAX_LINES: 400, // In-memory + panel lines
DEBUG_SHOW_PANEL: true, // Show floating debug console UI
DEBOUNCE_DELAY: 5000, // Bot typing protection
MAX_RETRIES: 2, // Retry attempts (=> up to MAX_RETRIES+1 total tries)
VERSION: '1.3.0',
VERSION: '1.3.2',
PROCESS_EXISTING: false, // If false, only process messages added after init (but see initial delayed scan)
ASSISTANT_ONLY: true, // Process assistant messages by default (your core use case)
PROCESS_EXISTING: false, // If false, only process *new* messages (no initial rescan)
ASSISTANT_ONLY: true, // Process assistant messages by default (core use case)
// Persistent dedupe window
DEDUPE_TTL_MS: 30 * 24 * 60 * 60 * 1000, // 30 days
@ -38,10 +42,195 @@
// Paste + submit behavior
APPEND_TRAILING_NEWLINE: true, // Add '\n' after pasted text
AUTO_SUBMIT: true, // Try to submit after pasting content
POST_PASTE_DELAY_MS: 250, // Small delay before submit to let editors settle
POST_PASTE_DELAY_MS: 250, // Delay before submit to let editors settle
SUBMIT_MODE: 'button_first', // 'button_first' | 'enter_only' | 'smart'
// Runtime toggles (live-updated by the debug panel)
RUNTIME: {
PAUSED: false, // Pause scanning + execution via panel
}
};
// ---------------------- Debug Console ----------------------
let RC_DEBUG = null;
class DebugConsole {
constructor(cfg) {
this.cfg = cfg;
this.buf = [];
this.loopCounts = new Map();
this.startedAt = Date.now();
this.panel = null;
if (cfg.DEBUG_SHOW_PANEL) this.mount();
this.info(`Debug console ready (level=${cfg.DEBUG_LEVEL})`);
}
// Levels: 1=ERROR, 2=WARN, 3=INFO, 4=VERB, 5=TRACE
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, 'VERB', msg, data); }
trace(msg, data) { this._log(5, 'TRACE', msg, data); }
command(action, status, extra={}) {
const icon = { detected:'👁️', parsing:'📝', validating:'✓', debouncing:'⏳', executing:'⚙️', complete:'✅', error:'❌' }[status] || '•';
this.info(`${icon} ${action} [${status}]`, extra);
}
nowIso() { return new Date().toISOString(); }
withinWatch() { return Date.now() - this.startedAt <= this.cfg.DEBUG_WATCH_MS; }
// Loop/ticker messages → suppress after 10 repeats or after WATCH window
logLoop(kind, msg) {
const k = `${kind}:${msg}`;
const cur = this.loopCounts.get(k) || 0;
if (!this.withinWatch() && kind !== 'WARN') return;
if (cur >= 10) return;
this.loopCounts.set(k, cur + 1);
const suffix = (cur + 1) > 1 ? ` (${cur + 1}x)` : '';
// default to INFO (visible at level 2+)
if (kind === 'ERROR') this.error(`${msg}${suffix}`);
else if (kind === 'WARN') this.warn(`${msg}${suffix}`);
else this.info(`${msg}${suffix}`);
}
copyLast(n=50) {
const lines = this.buf.slice(-n).map(e => `${e.ts} ${e.level.padEnd(5)} ${e.msg}${e.data? ' ' + JSON.stringify(e.data): ''}`).join('\n');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(lines).then(() => {
this.info(`Copied last ${Math.min(n, this.buf.length)} lines to clipboard`);
}).catch(e => this._fallbackCopy(lines, e));
} else {
this._fallbackCopy(lines);
}
}
_fallbackCopy(text, originalError = null) {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
if (ok) this.info(`Copied last ${text.split('\n').length} lines to clipboard (fallback)`);
else this.warn('Clipboard copy failed (fallback)');
} catch (e) {
this.warn('Clipboard copy failed', { error: originalError?.message || e.message });
}
}
setLevel(n) {
const lv = Math.max(0, Math.min(4, n)); // clamp 0..4
this.cfg.DEBUG_LEVEL = lv;
this.info(`Log level => ${lv}`);
}
_sanitize(data) {
if (!data) return null;
try {
if (data instanceof HTMLElement) return '[HTMLElement]';
if (typeof data === 'string' && data.length > 400) return data.slice(0,400)+'…';
if (typeof data === 'object') {
const clone = { ...data };
if (clone.element instanceof HTMLElement) clone.element = '[HTMLElement]';
return clone;
}
} catch {}
return data;
}
_log(numericLevel, levelName, msg, data) {
if (!this.cfg.DEBUG_MODE) return;
// Threshold map: 0=off, 1=ERROR, 2=+WARN+INFO, 3=+VERB, 4=+TRACE
const thresholdMap = { 0: 0, 1: 1, 2: 3, 3: 4, 4: 5 };
const threshold = thresholdMap[this.cfg.DEBUG_LEVEL] ?? 0;
if (numericLevel > threshold) return;
const entry = { ts: this.nowIso(), level: levelName, msg: String(msg), data: this._sanitize(data) };
this.buf.push(entry);
if (this.buf.length > this.cfg.DEBUG_MAX_LINES) this.buf.splice(0, this.buf.length - this.cfg.DEBUG_MAX_LINES);
// Keep console quiet unless verbose+ is enabled
if (this.cfg.DEBUG_LEVEL >= 3) {
const prefix = `[AI RC]`;
if (entry.data != null) console.log(prefix, entry.level, entry.msg, entry.data);
else console.log(prefix, entry.level, entry.msg);
}
if (this.panel) this._renderRow(entry);
}
mount() {
if (!document.body) {
setTimeout(() => this.mount(), 100);
return;
}
const root = document.createElement('div');
root.style.cssText = `
position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
width: 420px; max-height: 45vh; display: flex; flex-direction: column;
background: rgba(20,20,24,0.92); border:1px solid #3b3b46; border-radius: 8px;
color:#e5e7eb; font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
box-shadow: 0 16px 40px rgba(0,0,0,0.55); backdrop-filter: blur(4px);
`;
root.innerHTML = `
<div style="display:flex; gap:8px; align-items:center; padding:8px; border-bottom:1px solid #2c2c33">
<strong style="flex:1">AI Repo Commander Debug</strong>
<label style="display:flex;align-items:center;gap:4px;">Level
<select class="rc-level" style="background:#111827;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;">
<option value="0">off</option>
<option value="1">errors</option>
<option value="2" selected>info</option>
<option value="3">verbose</option>
<option value="4">trace</option>
</select>
</label>
<button class="rc-copy" title="Copy last 50 lines" style="padding:4px 6px;">Copy 50</button>
<button class="rc-pause" title="Pause/resume scanning" style="padding:4px 6px;">Pause</button>
<button class="rc-stop" title="Stop API calls" style="padding:4px 6px;background:#7f1d1d;color:#fff;border:1px solid #991b1b">STOP API</button>
</div>
<div class="rc-body" style="overflow:auto; padding:8px; display:block; flex:1"></div>
`;
document.body.appendChild(root);
this.panel = root;
const sel = root.querySelector('.rc-level');
sel.value = String(this.cfg.DEBUG_LEVEL);
sel.addEventListener('change', () => this.setLevel(parseInt(sel.value,10)));
root.querySelector('.rc-copy').addEventListener('click', () => this.copyLast(50));
const pauseBtn = root.querySelector('.rc-pause');
pauseBtn.addEventListener('click', () => {
this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED;
pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause';
pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : '';
pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : '';
this.info(`Runtime ${this.cfg.RUNTIME.PAUSED ? 'paused' : 'resumed'}`);
});
root.querySelector('.rc-stop').addEventListener('click', () => {
window.AI_REPO_STOP?.();
this.warn('Emergency STOP activated');
});
}
_renderRow(e) {
const body = this.panel.querySelector('.rc-body');
const row = document.createElement('div');
row.style.cssText = 'padding:4px 0;border-bottom:1px dashed #2a2a34;white-space:pre-wrap;word-break:break-word;';
row.textContent = `${e.ts} ${e.level.padEnd(5)} ${e.msg}${e.data? ' ' + JSON.stringify(e.data): ''}`;
body.appendChild(row);
while (body.children.length > this.cfg.DEBUG_MAX_LINES) body.firstChild.remove();
body.scrollTop = body.scrollHeight;
}
}
// ---------------------- Platform selectors ----------------------
const PLATFORM_SELECTORS = {
'chat.openai.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
@ -294,7 +483,7 @@
return false;
} catch (e) {
console.warn('[AI Repo Commander] pasteToComposer failed:', e);
RC_DEBUG?.warn('pasteToComposer failed', { error: String(e) });
return false;
}
}
@ -332,7 +521,6 @@
}
static extractCommandBlock(text) {
// Find ^%$bridge anywhere in the code block, prefer explicit '---' terminator
const patterns = [
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m,
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)(?=\n\s*$|\n---|\n```|$)/m,
@ -480,10 +668,8 @@
static _extractFilesArray(payload) {
const obj = Array.isArray(payload) ? payload[0] : payload;
// Common shapes: { result: { files: [...] } } or { files: [...] }
let files = obj?.result?.files ?? obj?.files ?? null;
if (!files) {
// Try to sniff: look for any array under result that looks like files
const res = obj?.result;
if (res) {
for (const [k, v] of Object.entries(res)) {
@ -497,7 +683,6 @@
}
static _formatFilesListing(files) {
// Accept strings or objects; prefer .path, else join directory/name
const pickPath = (f) => {
if (typeof f === 'string') return f;
if (typeof f?.path === 'string') return f.path;
@ -507,7 +692,6 @@
};
const lines = files.map(pickPath).filter(Boolean).sort();
// Friendly text block (fits most chat UIs)
return '```text\n' + lines.join('\n') + '\n```';
}
@ -521,7 +705,6 @@
details: data.message || 'Operation completed successfully'
});
// Auto-paste handlers
if (command.action === 'get_file') {
const body = this._extractGetFileBody(data);
if (typeof body === 'string' && body.length) {
@ -537,7 +720,6 @@
const listing = this._formatFilesListing(files);
await pasteAndMaybeSubmit(listing);
} else {
// Fallback: paste the whole payload as JSON for visibility
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
await pasteAndMaybeSubmit(fallback);
GM_notification({ title: 'AI Repo Commander', text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.', timeout: 5000 });
@ -571,22 +753,43 @@
// ---------------------- Monitor ----------------------
class CommandMonitor {
constructor() {
this.trackedMessages = new Map(); // id -> { element, originalText, state, lastUpdate }
this.trackedMessages = new Map(); // id -> { element, originalText, state, lastUpdate, startTime }
this.history = new CommandHistory();
this.observer = null;
this.currentPlatform = null;
this._idCounter = 0;
this.cleanupIntervalId = null;
this.initialize();
}
getReadableMessageId(element) {
this._idCounter += 1;
const id = `cmd-${this._idCounter}-${Math.random().toString(36).slice(2,6)}`;
if (element?.dataset) element.dataset.aiRcId = id;
return id;
}
extractAction(text) {
const m = /(^|\n)\s*action\s*:\s*([A-Za-z_][\w\-]*)/m.exec(text || '');
return m ? m[2] : 'unknown';
}
initialize() {
this.detectPlatform();
this.startObservation();
this.setupEmergencyStop();
this.log('AI Repo Commander initialized', CONFIG);
RC_DEBUG?.info('AI Repo Commander initialized', {
ENABLE_API: CONFIG.ENABLE_API,
DEBUG_MODE: CONFIG.DEBUG_MODE,
DEBOUNCE_DELAY: CONFIG.DEBOUNCE_DELAY,
MAX_RETRIES: CONFIG.MAX_RETRIES,
VERSION: CONFIG.VERSION
});
if (CONFIG.ENABLE_API) {
console.warn('[AI Repo Commander] API is enabled — you will be prompted for your bridge key on first command.');
RC_DEBUG?.warn('API is enabled — you will be prompted for your bridge key on first command.');
}
setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
// store interval id so STOP can clear it
this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
}
detectPlatform() {
@ -595,30 +798,68 @@
}
startObservation() {
// Throttled observer; only rescan if code blocks likely appeared
let scanPending = false;
const scheduleScan = () => {
if (scanPending) return;
scanPending = true;
setTimeout(() => { scanPending = false; this.scanMessages(); }, 100);
};
this.observer = new MutationObserver((mutations) => {
mutations.forEach((m) => {
m.addedNodes.forEach((node) => {
if (node.nodeType === 1) this.scanNode(node);
});
});
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.matches?.('pre, code') || node.querySelector?.('pre, code')) {
scheduleScan();
break;
}
}
}
});
this.observer.observe(document.body, { childList: true, subtree: true });
// Always do one delayed scan to catch initial render, even with PROCESS_EXISTING=false
setTimeout(() => {
this.log('Initial scan after page load');
this.scanMessages();
}, 2000);
// Respect PROCESS_EXISTING on initial scan
if (CONFIG.PROCESS_EXISTING) {
setTimeout(() => {
this.log('Deep scan of existing messages (PROCESS_EXISTING=true)');
RC_DEBUG?.info('Initial scan after page load (PROCESS_EXISTING=true)');
this.scanMessages();
}, 5000);
}, 1000);
} else {
RC_DEBUG?.info('Initial scan skipped (PROCESS_EXISTING=false)');
}
}
scanNode() { this.scanMessages(); }
scanMessages() {
if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.logLoop('loop', 'scan paused'); return; }
const messages = document.querySelectorAll(this.currentPlatform.messages);
let skipped = 0, found = 0;
messages.forEach((el) => {
if (!this.isAssistantMessage(el)) return;
if (el.dataset.aiRcProcessed) return;
const hit = this.findCommandInCodeBlock(el);
if (!hit) return;
const cmdText = hit.text;
if (this.history.has(cmdText)) {
el.dataset.aiRcProcessed = '1';
skipped++;
return;
}
el.dataset.aiRcProcessed = '1';
const id = this.getReadableMessageId(el);
this.trackMessage(el, cmdText, id);
found++;
});
if (skipped) RC_DEBUG?.logLoop('loop', `skipped already-executed (${skipped})`);
if (found) RC_DEBUG?.info(`Found ${found} new command(s)`);
}
isAssistantMessage(el) {
if (!CONFIG.ASSISTANT_ONLY) return true;
@ -634,11 +875,12 @@
return true;
}
// **HARDENED**: require header + action: to avoid partials
findCommandInCodeBlock(el) {
const blocks = el.querySelectorAll('pre code, pre, code');
for (const b of blocks) {
const txt = (b.textContent || '').trim();
if (/(^|\n)\s*\^%\$bridge\b/m.test(txt)) {
if (/(^|\n)\s*\^%\$bridge\b/m.test(txt) && /(^|\n)\s*action\s*:/m.test(txt)) {
return { blockElement: b, text: txt };
}
}
@ -646,10 +888,8 @@
}
getMessageId(element) {
if (element.dataset && element.dataset.aiRcId) return element.dataset.aiRcId;
const id = element.id || `${element.className}-${element.childElementCount}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
if (element.dataset) element.dataset.aiRcId = id;
return id;
// kept for compatibility
return this.getReadableMessageId(element);
}
reextractCommandText(element) {
@ -657,29 +897,8 @@
return hit ? hit.text : '';
}
scanMessages() {
const messages = document.querySelectorAll(this.currentPlatform.messages);
messages.forEach((el) => {
if (!this.isAssistantMessage(el)) return;
if (el.dataset.aiRcProcessed) return;
const hit = this.findCommandInCodeBlock(el);
if (!hit) return;
const cmdText = hit.text;
if (this.history.has(cmdText)) {
this.log('Skipping already-executed command (persistent history)');
return;
}
el.dataset.aiRcProcessed = '1';
this.trackMessage(el, cmdText, this.getMessageId(el));
});
}
trackMessage(element, text, messageId) {
this.log('New command detected in code block:', { messageId, preview: text.substring(0, 120) });
RC_DEBUG?.info('New command detected', { messageId, preview: text.substring(0, 120) });
this.trackedMessages.set(messageId, {
element, originalText: text, state: COMMAND_STATES.DETECTED, startTime: Date.now(), lastUpdate: Date.now()
});
@ -690,16 +909,21 @@
updateState(messageId, state) {
const msg = this.trackedMessages.get(messageId);
if (!msg) return;
const old = msg.state;
msg.state = state;
msg.lastUpdate = Date.now();
this.trackedMessages.set(messageId, msg);
this.log(`Message ${messageId} state updated to: ${state}`);
RC_DEBUG?.command(this.extractAction(msg.originalText), state, {
messageId, transition: `${old} -> ${state}`
});
}
async processCommand(messageId) {
if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.info('process paused, skipping', { messageId }); return; }
const started = Date.now();
try {
const message = this.trackedMessages.get(messageId);
if (!message) return;
if (!message) { RC_DEBUG?.error('Message not found', { messageId }); return; }
let parsed = CommandParser.parseYAMLCommand(message.originalText);
this.updateState(messageId, COMMAND_STATES.VALIDATING);
@ -712,40 +936,40 @@
await this.debounce();
const after = this.reextractCommandText(message.element);
if (!after) {
this.log('Command removed during debounce - aborting');
this.updateState(messageId, COMMAND_STATES.ERROR);
return;
}
if (!after) { this.updateState(messageId, COMMAND_STATES.ERROR); return; }
if (after !== before) {
this.log('Command changed during debounce - updating and re-debouncing once');
RC_DEBUG?.info('Command changed during debounce (re-validate)', { messageId });
message.originalText = after;
await this.debounce();
const finalTxt = this.reextractCommandText(message.element);
if (!finalTxt) {
this.log('Command removed after re-debounce - aborting');
this.updateState(messageId, COMMAND_STATES.ERROR);
return;
}
if (!finalTxt) { this.updateState(messageId, COMMAND_STATES.ERROR); return; }
message.originalText = finalTxt;
parsed = CommandParser.parseYAMLCommand(finalTxt);
validation = CommandParser.validateStructure(parsed);
if (!validation.isValid) throw new Error(`Final validation failed: ${validation.errors.join(', ')}`);
}
// **HARDENED**: pre-mark to avoid duplicate runs if DOM churns
this.history.mark(message.originalText);
this.updateState(messageId, COMMAND_STATES.EXECUTING);
await ExecutionManager.executeCommand(parsed, message.element);
this.history.mark(message.originalText);
const duration = Date.now() - started;
if (duration < 100) RC_DEBUG?.warn('Command completed suspiciously fast', { messageId, duration });
if (duration > 30000) RC_DEBUG?.warn('Command took unusually long', { messageId, duration });
this.updateState(messageId, COMMAND_STATES.COMPLETE);
} catch (error) {
this.log(`Command processing error: ${error.message}`);
const duration = Date.now() - started;
RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration });
this.updateState(messageId, COMMAND_STATES.ERROR);
const message = this.trackedMessages.get(messageId);
// Silent ignore for non-commands/partials to avoid noisy inline errors
if (/No valid command block|Missing required field:\s*action/i.test(error.message)) return;
if (message) {
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message });
}
@ -756,32 +980,52 @@
cleanupProcessedCommands() {
const now = Date.now();
let count = 0;
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);
count++;
}
}
if (count) RC_DEBUG?.info(`Cleaned ${count} processed entries`);
}
stopAllProcessing() {
this.trackedMessages.clear();
if (this.observer) this.observer.disconnect();
if (this.cleanupIntervalId) {
clearInterval(this.cleanupIntervalId);
this.cleanupIntervalId = null;
}
}
setupEmergencyStop() {
window.AI_REPO_STOP = () => {
// Critical: stop API + pause runtime + cancel inflight + clear interval
CONFIG.ENABLE_API = false;
CONFIG.RUNTIME.PAUSED = true;
for (const [id, msg] of this.trackedMessages.entries()) {
if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) {
RC_DEBUG?.error('Emergency stop - cancelling command', { messageId: id });
this.updateState(id, COMMAND_STATES.ERROR);
}
}
this.stopAllProcessing();
this.log('EMERGENCY STOP ACTIVATED');
GM_notification({ text: 'AI Repo Commander Emergency Stop Activated', title: 'Safety System', timeout: 5000 });
RC_DEBUG?.error('🚨 EMERGENCY STOP ACTIVATED 🚨');
GM_notification({ text: 'All command processing stopped', title: 'Emergency Stop', timeout: 5000 });
};
}
log(...args) { if (CONFIG.DEBUG_MODE) console.log('[AI Repo Commander]', ...args); }
log(...args) {
const [msg, data] = (typeof args[0] === 'string') ? [args[0], args[1]] : ['(log)', args];
RC_DEBUG?.verbose(msg, data);
}
}
// ---------------------- Test commands ----------------------
// ---------------------- Test commands (unchanged) ----------------------
const TEST_COMMANDS = {
validUpdate:
`\
@ -821,6 +1065,8 @@ path: .
// ---------------------- Init ----------------------
let commandMonitor;
function initializeRepoCommander() {
if (!RC_DEBUG) RC_DEBUG = new DebugConsole(CONFIG);
if (!commandMonitor) {
commandMonitor = new CommandMonitor();
window.AI_REPO_COMMANDER = {
@ -831,10 +1077,10 @@ path: .
history: commandMonitor.history,
submitComposer // expose for quick testing
};
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');
console.log('Reset history: window.AI_REPO_COMMANDER.history.reset() or AI_REPO_CLEAR_HISTORY()');
RC_DEBUG?.info('AI Repo Commander fully initialized');
RC_DEBUG?.info('API Enabled:', { value: CONFIG.ENABLE_API });
RC_DEBUG?.info('Test commands available in window.AI_REPO_COMMANDER.test');
RC_DEBUG?.info('Reset history with window.AI_REPO_COMMANDER.history.reset() or AI_REPO_CLEAR_HISTORY()');
}
}