diff --git a/src/debug-panel.js b/src/debug-panel.js
new file mode 100644
index 0000000..f4ab468
--- /dev/null
+++ b/src/debug-panel.js
@@ -0,0 +1,70 @@
+// ==DEBUG PANEL START==
+// Depends on: config.js, logger.js, queue.js
+(function () {
+ const cfg = () => window.AI_REPO_CONFIG;
+ const log = () => window.AI_REPO_LOGGER;
+
+ class DebugPanel {
+ constructor() { this.root = null; }
+ mount() {
+ if (!cfg().get('debug.showPanel')) return;
+ if (this.root) return;
+ const root = document.createElement('div');
+ root.style.cssText = `
+ position:fixed; right:16px; bottom:16px; z-index:2147483647; width:460px; max-height:55vh;
+ display:flex; flex-direction:column; background:rgba(20,20,24,.92); border:1px solid #3b3b46; border-radius:8px;
+ color:#e5e7eb; font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; box-shadow:0 16px 40px rgba(0,0,0,.55);
+ `;
+ root.innerHTML = `
+
+ AI Repo Commander
+
+
+
+
+
+ `;
+ document.body.appendChild(root);
+ this.root = root;
+ this.body = root.querySelector('[data-body]');
+ this._wire();
+ this._tick();
+ }
+ _wire() {
+ this.root.addEventListener('click', (e) => {
+ const btn = e.target.closest('button[data-act]'); if (!btn) return;
+ const act = btn.getAttribute('data-act');
+ if (act === 'copy') navigator.clipboard?.writeText(log().getRecentLogs(200));
+ if (act === 'pause') {
+ const paused = !cfg().get('runtime.paused'); cfg().set('runtime.paused', paused);
+ btn.textContent = paused ? 'Resume' : 'Pause';
+ log().info(paused ? 'Paused' : 'Resumed');
+ }
+ if (act === 'clearq') window.AI_REPO_QUEUE.clear();
+ });
+ // queue badge
+ const q = window.AI_REPO_QUEUE;
+ if (q) q.onSizeChange = (n) => this._toast(`Queue: ${n}`);
+ }
+ _toast(msg) {
+ if (!this.root) return;
+ const t = document.createElement('div');
+ t.textContent = msg;
+ t.style.cssText = 'position:absolute; right:12px; bottom:12px; padding:6px 10px; background:#111827; color:#e5e7eb; border:1px solid #374151; border-radius:6px;';
+ this.root.appendChild(t); setTimeout(() => t.remove(), 1000);
+ }
+ _tick() {
+ if (!this.body) return;
+ const rows = window.AI_REPO_LOGGER?.buffer?.slice(-80) || [];
+ this.body.innerHTML = rows.map(e => `${e.timestamp} ${e.level} ${e.message}
`).join('');
+ requestAnimationFrame(() => this._tick());
+ }
+ }
+
+ const panel = new DebugPanel();
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => panel.mount());
+ else panel.mount();
+
+ window.AI_REPO_DEBUG_PANEL = panel;
+})();
+// ==DEBUG PANEL END==
diff --git a/src/detector.js b/src/detector.js
new file mode 100644
index 0000000..2231944
--- /dev/null
+++ b/src/detector.js
@@ -0,0 +1,131 @@
+// ==DETECTOR START==
+// Depends on: config.js, logger.js, queue.js, command-parser.js, command-executor.js, storage.js
+(function () {
+ const cfg = () => window.AI_REPO_CONFIG;
+ const log = () => window.AI_REPO_LOGGER;
+
+ function extractAllBlocks(text) {
+ const out = []; const re = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/gm;
+ let m; while ((m = re.exec(text)) !== null) out.push(m[0]);
+ return out;
+ }
+
+ function isAssistantMsg(el) {
+ const sels = [
+ '[data-message-author-role="assistant"]',
+ '.chat-message:not([data-message-author-role="user"])',
+ '.message-content'
+ ];
+ return sels.some(s => el.matches?.(s) || el.querySelector?.(s));
+ }
+
+ async function settleText(el, initial, windowMs, pollMs) {
+ let deadline = Date.now() + windowMs;
+ let last = initial;
+ while (Date.now() < deadline) {
+ await new Promise(r => setTimeout(r, pollMs));
+ const fresh = el.textContent || '';
+ const blocks = extractAllBlocks(fresh);
+ const pick = blocks.join('\n'); // if multiple, concatenate for stability check (we’ll split later)
+ if (pick === last && pick) continue; // stable—keep waiting out the window
+ if (pick && pick !== last) { last = pick; deadline = Date.now() + windowMs; }
+ }
+ return last;
+ }
+
+ class Detector {
+ constructor() {
+ this.observer = null;
+ this.processed = new WeakSet();
+ this.clusterLookahead = 3;
+ this.clusterWindowMs = 1000;
+ }
+ start() {
+ this.observer = new MutationObserver((mutations) => {
+ if (cfg().get('runtime.paused')) return;
+ let should = false;
+ for (const m of mutations) {
+ if (m.type === 'childList') {
+ for (const n of m.addedNodes) {
+ if (n.nodeType === 1 && isAssistantMsg(n)) { this._handle(n); should = true; }
+ }
+ }
+ if (m.type === 'characterData') {
+ const el = m.target?.parentElement;
+ if (el && isAssistantMsg(el)) should = true;
+ }
+ }
+ if (should) {/* no-op: _handle already queued */}
+ });
+ this.observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true });
+ if (cfg().get('ui.processExisting')) {
+ document.querySelectorAll('[data-message-author-role], .chat-message, .message-content')
+ .forEach(el => isAssistantMsg(el) && this._handle(el));
+ }
+ log().info('Detector started');
+ }
+
+ async _handle(el) {
+ if (this.processed.has(el)) return;
+ this.processed.add(el);
+
+ // Debounce complete generation
+ const debounce = cfg().get('execution.debounceDelay') || 0;
+ if (debounce > 0) await new Promise(r => setTimeout(r, debounce));
+
+ // Settle
+ const baseText = el.textContent || '';
+ const stable = await settleText(el, baseText, cfg().get('execution.settleCheckMs') || 1200, cfg().get('execution.settlePollMs') || 250);
+ const blocks = extractAllBlocks(stable);
+ if (!blocks.length) { this.processed.delete(el); return; } // not a command after all
+
+ const maxPerMsg = cfg().get('queue.maxPerMessage') || 5;
+ blocks.slice(0, maxPerMsg).forEach((cmdText, idx) => this._enqueueOne(el, cmdText, idx));
+
+ // Cluster rescan: look ahead a few assistant messages for chained blocks
+ setTimeout(() => this._clusterRescan(el), this.clusterWindowMs);
+ }
+
+ _enqueueOne(el, commandText, idx) {
+ const history = window.AI_REPO_HISTORY;
+ if (history.isProcessed(el, idx)) {
+ this._addRunAgain(el, commandText, idx);
+ return;
+ }
+ history.markProcessed(el, idx);
+
+ window.AI_REPO_QUEUE.push(async () => {
+ try {
+ const parsed = window.AI_REPO_PARSER.parse(commandText);
+ const v = window.AI_REPO_PARSER.validate(parsed);
+ if (!v.isValid) throw new Error(`Validation failed: ${v.errors.join(', ')}`);
+ if (v.example) { log().info('Example command skipped'); return; }
+ await window.AI_REPO_EXECUTOR.execute(parsed, el, `[${idx + 1}] ${parsed.action}`);
+ } catch (e) {
+ log().error('Command failed', { error: e.message });
+ this._addRunAgain(el, commandText, idx);
+ }
+ });
+ }
+
+ _addRunAgain(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._enqueueOne(el, commandText, idx));
+ el.appendChild(btn);
+ }
+
+ _clusterRescan(anchor) {
+ let scanned = 0; let cur = anchor.nextElementSibling;
+ while (cur && scanned < this.clusterLookahead) {
+ if (!isAssistantMsg(cur)) break;
+ if (!this.processed.has(cur)) this._handle(cur);
+ scanned++; cur = cur.nextElementSibling;
+ }
+ }
+ }
+
+ window.AI_REPO_DETECTOR = new Detector();
+})();
+// ==DETECTOR END==
diff --git a/src/fingerprint-strong.js b/src/fingerprint-strong.js
new file mode 100644
index 0000000..185aa21
--- /dev/null
+++ b/src/fingerprint-strong.js
@@ -0,0 +1,35 @@
+// ==FINGERPRINT (drop-in utility) ==
+(function(){
+ function norm(s){ return (s||'').replace(/\r/g,'').replace(/\u200b/g,'').replace(/[ \t]+\n/g,'\n').trim(); }
+ function hash(s){ let h=5381; for(let i=0;i>>0).toString(36); }
+ function commandLikeText(el){
+ const blocks = el.querySelectorAll('pre code, pre, code');
+ for (const b of blocks) {
+ const t = norm(b.textContent || '');
+ if (/@end@\s*$/m.test(t) && /(^|\n)\s*@bridge@\b/m.test(t) && /(^|\n)\s*action\s*:/m.test(t)) return t;
+ }
+ return norm((el.textContent || '').slice(0, 2000));
+ }
+ function prevContextHash(el) {
+ const list = Array.from(document.querySelectorAll('[data-message-author-role], .chat-message, .message-content'));
+ const idx = list.indexOf(el); if (idx <= 0) return '0';
+ let rem = 2000, buf = '';
+ for (let i=idx-1; i>=0 && rem>0; i--){
+ const t = norm(list[i].textContent || ''); if (!t) continue;
+ const take = t.slice(-rem); buf = take + buf; rem -= take.length;
+ }
+ return hash(buf.slice(-2000));
+ }
+ function intraPrefixHash(el){
+ const t = el.textContent || '';
+ const m = t.match(/@bridge@[\s\S]*?@end@/m);
+ const endIdx = m ? t.indexOf(m[0]) : t.length;
+ return hash(norm(t.slice(Math.max(0, endIdx - 2000), endIdx)));
+ }
+ window.AI_REPO_FINGERPRINT = function(el){
+ const ch = hash(commandLikeText(el).slice(0, 2000));
+ const ph = prevContextHash(el);
+ const ih = intraPrefixHash(el);
+ return `ch:${ch}|ph:${ph}|ih:${ih}`;
+ };
+})();
diff --git a/src/main.js b/src/main.js
index 878925c..9d102e9 100644
--- a/src/main.js
+++ b/src/main.js
@@ -154,6 +154,8 @@
} else {
window.AI_REPO_MAIN = new AIRepoCommander();
window.AI_REPO_MAIN.initialize();
+ // Kick off the advanced detector (restores settle/debounce, multi-block, cluster rescan)
+ window.AI_REPO_DETECTOR?.start();
}
})();
// ==MAIN END==
diff --git a/src/paste-submit.js b/src/paste-submit.js
new file mode 100644
index 0000000..eea8af8
--- /dev/null
+++ b/src/paste-submit.js
@@ -0,0 +1,139 @@
+// ==PASTE SUBMIT START==
+// Depends on: config.js, logger.js
+/* global GM_setClipboard */
+(function () {
+ const cfg = () => window.AI_REPO_CONFIG;
+ const log = () => window.AI_REPO_LOGGER;
+
+ function findComposer() {
+ const sels = [
+ '#prompt-textarea',
+ '.ProseMirror#prompt-textarea',
+ '.ProseMirror[role="textbox"][contenteditable="true"]',
+ '[data-testid="composer"] [contenteditable="true"][role="textbox"]',
+ 'main [contenteditable="true"][role="textbox"]',
+ 'textarea[data-testid="input-area"]',
+ '[contenteditable="true"][aria-label*="Message"]',
+ 'textarea',
+ '[contenteditable="true"]'
+ ];
+ for (const s of sels) {
+ const el = document.querySelector(s);
+ if (!el) continue;
+ const st = window.getComputedStyle(el);
+ if (st.display === 'none' || st.visibility === 'hidden') continue;
+ if (el.offsetParent === null && st.position !== 'fixed') continue;
+ return el;
+ }
+ return null;
+ }
+
+ function findSendButton(scopeEl) {
+ const scope = scopeEl?.closest('form, [data-testid="composer"], main, body') || document;
+ const sels = [
+ 'button[data-testid="send-button"]',
+ '#composer-submit-button',
+ 'button[aria-label*="Send prompt"]',
+ 'button[aria-label*="Send message"]',
+ 'button[aria-label="Send"]',
+ 'button[aria-label*="Send"]',
+ 'form button'
+ ];
+ for (const s of sels) {
+ const b = scope.querySelector(s) || document.querySelector(s);
+ if (!b) continue;
+ const st = window.getComputedStyle(b);
+ const disabled = b.disabled || b.getAttribute('aria-disabled') === 'true';
+ const hidden = st.display === 'none' || st.visibility === 'hidden';
+ const notRendered = b.offsetParent === null && st.position !== 'fixed';
+ if (!disabled && !hidden && !notRendered) return b;
+ }
+ return null;
+ }
+
+ function pressEnter(el) {
+ for (const t of ['keydown','keypress','keyup']) {
+ const ok = el.dispatchEvent(new KeyboardEvent(t, { key:'Enter', code:'Enter', keyCode:13, which:13, bubbles:true, cancelable:true }));
+ if (!ok) return false;
+ }
+ return true;
+ }
+
+ async function waitReady(timeoutMs = 12000) {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ const el = findComposer();
+ if (el) {
+ const current = (el.textContent || el.value || '').trim();
+ const busy = el.closest('form, [data-testid="composer"], main, body')?.querySelector('[aria-busy="true"], [data-state="loading"], .typing-indicator');
+ if (!busy && current.length === 0) return true;
+ }
+ await new Promise(r => setTimeout(r, 200));
+ }
+ return false;
+ }
+
+ function pasteInto(el, text) {
+ const payload = cfg().get('ui.appendTrailingNewline') ? (text.endsWith('\n') ? text : text + '\n') : text;
+ try {
+ // ClipboardEvent path
+ const dt = new DataTransfer(); dt.setData('text/plain', payload);
+ const evt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
+ if (el.dispatchEvent(evt) && !evt.defaultPrevented) return true;
+ } catch {}
+ // ProseMirror path
+ if (el.classList?.contains('ProseMirror')) {
+ const node = document.createTextNode('\n' + payload.replace(/\n?$/,'\n') + '\n');
+ el.innerHTML = ''; el.appendChild(node);
+ el.dispatchEvent(new Event('input', { bubbles: true }));
+ return true;
+ }
+ // Selection/contentEditable path
+ try {
+ if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
+ const sel = window.getSelection(); if (sel && sel.rangeCount === 0) {
+ const r = document.createRange(); r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
+ }
+ const range = window.getSelection()?.getRangeAt(0);
+ if (range) {
+ range.deleteContents(); const node = document.createTextNode(payload);
+ range.insertNode(node); range.setStartAfter(node); range.setEndAfter(node);
+ el.dispatchEvent(new Event('input', { bubbles: true }));
+ return true;
+ }
+ }
+ } catch {}
+ // Textarea path
+ if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
+ el.value = payload; el.dispatchEvent(new Event('input', { bubbles: true })); return true;
+ }
+ // Fallback to clipboard
+ try {
+ if (typeof GM_setClipboard === 'function') {
+ GM_setClipboard(payload, { type:'text', mimetype:'text/plain' });
+ alert('Content copied to clipboard. Press Ctrl/Cmd+V to paste.');
+ return true;
+ }
+ } catch {}
+ return false;
+ }
+
+ async function submitToComposer(text) {
+ const auto = !!cfg().get('ui.autoSubmit');
+ const ok = await waitReady(cfg().get('execution.settleCheckMs') || 1200);
+ if (!ok) { log()?.warn('Composer not ready'); return false; }
+
+ const el = findComposer(); if (!el) { log()?.warn('Composer not found'); return false; }
+ if (text && !pasteInto(el, text)) { log()?.warn('Paste failed'); return false; }
+
+ await new Promise(r => setTimeout(r, cfg().get('ui.postPasteDelayMs') || 600));
+ if (!auto) return true;
+
+ const btn = findSendButton(el);
+ if (btn) { btn.click(); return true; }
+ return pressEnter(el);
+ }
+
+ window.AI_REPO_PASTE = { submitToComposer };
+})();
+// ==PASTE SUBMIT END==
diff --git a/src/queue.js b/src/queue.js
new file mode 100644
index 0000000..f543374
--- /dev/null
+++ b/src/queue.js
@@ -0,0 +1,43 @@
+// ==QUEUE START==
+// Depends on: config.js, logger.js
+(function () {
+ class ExecutionQueue {
+ constructor(opts = {}) {
+ const cfg = window.AI_REPO_CONFIG;
+ this.minDelayMs = opts.minDelayMs ?? cfg.get('queue.minDelayMs') ?? 1500;
+ this.maxPerMinute = opts.maxPerMinute ?? cfg.get('queue.maxPerMinute') ?? 15;
+ this.q = [];
+ this.running = false;
+ this.timestamps = [];
+ this.onSizeChange = null;
+ }
+ push(task) {
+ this.q.push(task);
+ this.onSizeChange?.(this.q.length);
+ if (!this.running) void this._drain();
+ }
+ clear() { this.q.length = 0; this.onSizeChange?.(0); }
+ size() { return this.q.length; }
+ _withinBudget() {
+ const now = Date.now();
+ this.timestamps = this.timestamps.filter(t => now - t < 60_000);
+ return this.timestamps.length < this.maxPerMinute;
+ }
+ async _drain() {
+ if (this.running) return;
+ this.running = true;
+ while (this.q.length) {
+ while (!this._withinBudget()) await this._delay(400);
+ const fn = this.q.shift();
+ this.onSizeChange?.(this.q.length);
+ try { await fn(); } catch (e) { window.AI_REPO_LOGGER?.warn('Queue task error', { error: String(e) }); }
+ this.timestamps.push(Date.now());
+ await this._delay(this.minDelayMs);
+ }
+ this.running = false;
+ }
+ _delay(ms){ return new Promise(r => setTimeout(r, ms)); }
+ }
+ window.AI_REPO_QUEUE = new ExecutionQueue();
+})();
+// ==QUEUE END==
diff --git a/src/response-buffer.js b/src/response-buffer.js
new file mode 100644
index 0000000..1cc049f
--- /dev/null
+++ b/src/response-buffer.js
@@ -0,0 +1,71 @@
+// ==RESPONSE BUFFER START==
+// Depends on: config.js, logger.js, queue.js, paste-submit.js
+(function () {
+ function chunkByLines(s, limit) {
+ const out = []; let start = 0;
+ while (start < s.length) {
+ const soft = s.lastIndexOf('\n', Math.min(start + limit, s.length));
+ const end = soft > start ? soft + 1 : Math.min(start + limit, s.length);
+ out.push(s.slice(start, end)); start = end;
+ }
+ return out;
+ }
+ function isSingleFence(s){ return /^```[^\n]*\n[\s\S]*\n```$/.test(s.trim()); }
+ function splitRespectingFence(text, limit) {
+ const t = text.trim(); if (!isSingleFence(t)) return chunkByLines(text, limit);
+ const m = /^```([^\n]*)\n([\s\S]*)\n```$/.exec(t);
+ const lang = (m?.[1] || 'text').trim(); const inner = m?.[2] ?? '';
+ const chunks = chunkByLines(inner, limit - 16 - lang.length);
+ return chunks.map(c => '```' + lang + '\n' + c.replace(/\n?$/, '\n') + '```');
+ }
+
+ class ResponseBuffer {
+ constructor() {
+ this.pending = []; this.timer = null; this.flushing = false;
+ }
+ push({ label, content }) {
+ if (!content) return;
+ this.pending.push({ label, content });
+ this._schedule();
+ }
+ _schedule() {
+ clearTimeout(this.timer);
+ this.timer = setTimeout(() => this.flush(), 500);
+ }
+ _build() {
+ const showHeadings = true; // readable by default
+ const parts = [];
+ for (const { label, content } of this.pending) {
+ if (showHeadings && label) parts.push(`### ${label}\n`);
+ parts.push(String(content).trimEnd(), '');
+ }
+ return parts.join('\n');
+ }
+ async flush() {
+ if (this.flushing || !this.pending.length) return;
+ this.flushing = true;
+ const toPaste = this._build(); this.pending.length = 0;
+ try {
+ const limit = 250_000;
+ if (toPaste.length > limit) {
+ const chunks = splitRespectingFence(toPaste, limit);
+ chunks.forEach((c, i) => {
+ const header = `### Part ${i+1}/${chunks.length}\n`;
+ const payload = header + c;
+ window.AI_REPO_QUEUE.push(async () => {
+ await window.AI_REPO_PASTE.submitToComposer(payload);
+ });
+ });
+ } else {
+ window.AI_REPO_QUEUE.push(async () => {
+ await window.AI_REPO_PASTE.submitToComposer(toPaste);
+ });
+ }
+ } finally {
+ this.flushing = false;
+ }
+ }
+ }
+ window.AI_REPO_RESPONSES = new ResponseBuffer();
+})();
+// ==RESPONSE BUFFER END==
diff --git a/src/storage.js b/src/storage.js
index 96873d9..2905aed 100644
--- a/src/storage.js
+++ b/src/storage.js
@@ -34,11 +34,10 @@
}
_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)}`;
+ const base = window.AI_REPO_FINGERPRINT ? window.AI_REPO_FINGERPRINT(el) : this._hash((el.textContent||'').slice(0,1000));
+ return `${base}|idx:${idx}`;
}
+
_hash(str) {
let h = 5381;
for (let i = 0; i < Math.min(str.length, 1000); i++) h = ((h << 5) + h) ^ str.charCodeAt(i);
diff --git a/src/userscript-bootstrap.user.js b/src/userscript-bootstrap.user.js
index 0663cd9..afbdb2b 100644
--- a/src/userscript-bootstrap.user.js
+++ b/src/userscript-bootstrap.user.js
@@ -1,13 +1,15 @@
// ==UserScript==
-// @name AI Repo Commander (Modular)
+// @name AI Repo Commander (Full Features)
// @namespace http://tampermonkey.net/
-// @version 2.0.0
-// @description Modularized AI Repo Commander
+// @version 2.1.0
+// @description Full modular AI Repo Commander with all features
+// @author Robert Dickson
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @match https://claude.ai/*
// @match https://gemini.google.com/*
// @grant GM_xmlhttpRequest
+// @grant GM_setClipboard
// @connect n8n.brrd.tech
// @connect *
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/config.js
@@ -16,4 +18,9 @@
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/command-parser.js
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/command-executor.js
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/main.js
-// ==/UserScript==
+// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/queue.js
+// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/response-buffer.js
+// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/paste-submit.js
+// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/detector.js
+// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/debug-panel.js
+// ==/UserScript==
\ No newline at end of file