diff --git a/src/command-executor.js b/src/command-executor.js index 826372d..3a0b562 100644 --- a/src/command-executor.js +++ b/src/command-executor.js @@ -1,7 +1,30 @@ // ==COMMAND EXECUTOR START== /* global GM_xmlhttpRequest */ +/* global GM_notification */ (function () { + /** + * @typedef {Object} RepoCommand + * @property {string} action + * @property {string} [repo] + * @property {string} [owner] + * @property {string} [path] + * @property {string} [content] + * @property {string} [commit_message] + * @property {string} [url] + * @property {boolean} [example] + */ + + /** + * @typedef {Object} FileEntry + * @property {string} [path] + * @property {string} [name] + */ class CommandExecutor { + /** + * @param {RepoCommand} command + * @param {Element} sourceElement + * @param {string} [label] + */ static async execute(command, sourceElement, label = '') { const log = window.AI_REPO_LOGGER; const cfg = window.AI_REPO_CONFIG; @@ -97,6 +120,7 @@ } static _handleListFiles(data, label) { + /** @type {Array} */ 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```'; diff --git a/src/detector.js b/src/detector.js index 2231944..7c20e45 100644 --- a/src/detector.js +++ b/src/detector.js @@ -11,12 +11,12 @@ } function isAssistantMsg(el) { - const sels = [ + const selectors = [ '[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)); + return selectors.some(s => el.matches?.(s) || el.querySelector?.(s)); } async function settleText(el, initial, windowMs, pollMs) { @@ -47,7 +47,7 @@ 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 (n.nodeType === 1 && isAssistantMsg(n)) { void this._handle(n); should = true; } } } if (m.type === 'characterData') { @@ -60,7 +60,7 @@ 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)); + .forEach(el => isAssistantMsg(el) && void this._handle(el)); } log().info('Detector started'); } @@ -98,7 +98,12 @@ 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.isValid) { + // Handle validation failures inline instead of throwing; avoid local throw/catch + try { log().warn?.('Validation failed', { errors: v.errors }); } catch {} + this._addRunAgain(el, commandText, idx); + return; + } if (v.example) { log().info('Example command skipped'); return; } await window.AI_REPO_EXECUTOR.execute(parsed, el, `[${idx + 1}] ${parsed.action}`); } catch (e) { @@ -120,7 +125,7 @@ let scanned = 0; let cur = anchor.nextElementSibling; while (cur && scanned < this.clusterLookahead) { if (!isAssistantMsg(cur)) break; - if (!this.processed.has(cur)) this._handle(cur); + if (!this.processed.has(cur)) void this._handle(cur); scanned++; cur = cur.nextElementSibling; } } diff --git a/src/fingerprint-strong.js b/src/fingerprint-strong.js index 185aa21..eee9145 100644 --- a/src/fingerprint-strong.js +++ b/src/fingerprint-strong.js @@ -1,7 +1,14 @@ // ==FINGERPRINT (drop-in utility) == (function(){ + const MSG_SELECTORS = [ + '[data-message-author-role="assistant"]', + '.chat-message:not([data-message-author-role="user"])', + '.message-content' + ]; + 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) { @@ -10,8 +17,9 @@ } 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 list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))); const idx = list.indexOf(el); if (idx <= 0) return '0'; let rem = 2000, buf = ''; for (let i=idx-1; i>=0 && rem>0; i--){ @@ -20,16 +28,57 @@ } 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){ + + function domHint(node) { + if (!node) return ''; + const id = node.id || ''; + const cls = (node.className && typeof node.className === 'string') ? node.className.split(' ')[0] : ''; + return `${node.tagName || ''}#${id}.${cls}`.slice(0, 40); + } + + function ordinalForKey(el, key) { + const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))); + let n = 0; + for (const node of list) { + const nodeKey = node === el ? key : (() => { + const ch = hash(commandLikeText(node).slice(0, 2000)); + const ph = prevContextHash(node); + const ih = intraPrefixHash(node); + return `ch:${ch}|ph:${ph}|ih:${ih}`; + })(); + if (nodeKey === key) { + if (node === el) return n; + n++; + } + } + return n; + } + + function fingerprintElement(el){ const ch = hash(commandLikeText(el).slice(0, 2000)); const ph = prevContextHash(el); const ih = intraPrefixHash(el); - return `ch:${ch}|ph:${ph}|ih:${ih}`; - }; + const dh = hash(domHint(el)); + const key = `ch:${ch}|ph:${ph}|ih:${ih}`; + const n = ordinalForKey(el, key); + return `${key}|hint:${dh}|n:${n}`; + } + + function getStableFingerprint(el) { + if (el?.dataset?.aiRcStableFp) return el.dataset.aiRcStableFp; + const fp = fingerprintElement(el); + try { if (el && el.dataset) el.dataset.aiRcStableFp = fp; } catch {} + return fp; + } + + // Expose both for backward compatibility + window.AI_REPO_FINGERPRINT = fingerprintElement; + window.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint; })(); diff --git a/src/main.js b/src/main.js index fa1cc1b..7b4a186 100644 --- a/src/main.js +++ b/src/main.js @@ -69,7 +69,7 @@ if (history.isProcessed(el, idx)) { this.addRetryButton(el, cmdText, idx); } else { - this.run(el, cmdText, idx); + void this.run(el, cmdText, idx); } }); } @@ -126,12 +126,13 @@ } exposeAPI() { - window.AI_REPO_COMMANDER = { + // Public API (short name) + window.AI_REPO = { version: config.get('meta.version'), - config, - logger, + config: config, + logger: logger, history, - pause: () => { config.set('runtime.paused', true); logger.info('Paused'); }, + 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'); } }; @@ -147,9 +148,21 @@ logger.error(`🚨 EMERGENCY STOP: cancelled ${queuedCount} queued command(s)`); logger.error('API disabled and scanning paused'); }; + + // Bridge key setter + window.AI_REPO_SET_KEY = function(k) { + if (typeof k === 'string' && k.trim()) { + config.set('api.bridgeKey', k.trim()); + logger.info('Bridge key updated'); + return true; + } + logger.warn('Invalid bridge key'); + return false; + }; } delay(ms) { return new Promise(r => setTimeout(r, ms)); } + destroy() { this.observer?.disconnect(); this.processed = new WeakSet(); diff --git a/src/storage.js b/src/storage.js index 2905aed..7432326 100644 --- a/src/storage.js +++ b/src/storage.js @@ -34,7 +34,12 @@ } _fingerprint(el, idx) { - const base = window.AI_REPO_FINGERPRINT ? window.AI_REPO_FINGERPRINT(el) : this._hash((el.textContent||'').slice(0,1000)); + // Use stable fingerprinting if available (caches on element.dataset) + const base = window.AI_REPO_STABLE_FINGERPRINT + ? window.AI_REPO_STABLE_FINGERPRINT(el) + : (window.AI_REPO_FINGERPRINT + ? window.AI_REPO_FINGERPRINT(el) + : this._hash((el.textContent||'').slice(0,1000))); return `${base}|idx:${idx}`; } diff --git a/src/userscript-bootstrap.user.js b/src/userscript-bootstrap.user.js index afbdb2b..aed7f5c 100644 --- a/src/userscript-bootstrap.user.js +++ b/src/userscript-bootstrap.user.js @@ -9,18 +9,20 @@ // @match https://claude.ai/* // @match https://gemini.google.com/* // @grant GM_xmlhttpRequest +// @grant GM_notification // @grant GM_setClipboard // @connect n8n.brrd.tech // @connect * // @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/config.js // @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/logger.js +// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/fingerprint-strong.js // @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/storage.js // @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 // @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/command-executor.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 +// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/main.js // ==/UserScript== \ No newline at end of file