Update src/ai-repo-commander.user.js
handleSuccess() now handles list_files by extracting a files array from typical n8n payloads and pasting a neat text code block. If it can’t find a files array, it pastes the raw JSON so you can see what came back. Added auto-submit after paste (AUTO_SUBMIT: true). It: tries clicking a visible Send button (button[data-testid="send-button"] or aria-label*="Send"), falls back to synthesizing Enter key events. Added APPEND_TRAILING_NEWLINE: true (helps some editors pick up a final input change reliably). Removed the unused scanExistingMessages() and updated a comment in the parser to match actual behavior.
This commit is contained in:
parent
36951c5b16
commit
e75a06a751
|
|
@ -1,8 +1,8 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name AI Repo Commander
|
// @name AI Repo Commander
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 1.2.1
|
// @version 1.3.0
|
||||||
// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe and robust paste
|
// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe, robust paste, and optional auto-submit
|
||||||
// @author Your Name
|
// @author Your Name
|
||||||
// @match https://chat.openai.com/*
|
// @match https://chat.openai.com/*
|
||||||
// @match https://chatgpt.com/*
|
// @match https://chatgpt.com/*
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
DEBUG_MODE: true, // Console logs
|
DEBUG_MODE: true, // Console logs
|
||||||
DEBOUNCE_DELAY: 5000, // Bot typing protection
|
DEBOUNCE_DELAY: 5000, // Bot typing protection
|
||||||
MAX_RETRIES: 2, // Retry attempts (=> up to MAX_RETRIES+1 total tries)
|
MAX_RETRIES: 2, // Retry attempts (=> up to MAX_RETRIES+1 total tries)
|
||||||
VERSION: '1.2.1',
|
VERSION: '1.3.0',
|
||||||
|
|
||||||
PROCESS_EXISTING: false, // If false, only process messages added after init (but see initial delayed scan)
|
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)
|
ASSISTANT_ONLY: true, // Process assistant messages by default (your core use case)
|
||||||
|
|
@ -33,7 +33,13 @@
|
||||||
|
|
||||||
// Housekeeping
|
// Housekeeping
|
||||||
CLEANUP_AFTER_MS: 30000, // Drop COMPLETE/ERROR entries after 30s
|
CLEANUP_AFTER_MS: 30000, // Drop COMPLETE/ERROR entries after 30s
|
||||||
CLEANUP_INTERVAL_MS: 60000 // Sweep cadence
|
CLEANUP_INTERVAL_MS: 60000, // Sweep cadence
|
||||||
|
|
||||||
|
// 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
|
||||||
|
SUBMIT_MODE: 'button_first', // 'button_first' | 'enter_only' | 'smart'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------- Platform selectors ----------------------
|
// ---------------------- Platform selectors ----------------------
|
||||||
|
|
@ -55,9 +61,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const FIELD_VALIDATORS = {
|
const FIELD_VALIDATORS = {
|
||||||
// allow "owner/repo" or just "repo"
|
|
||||||
repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v),
|
repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v),
|
||||||
// minimal traversal guard: no absolute paths, no backslashes, no ".."
|
|
||||||
path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
|
path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
|
||||||
action: (v) => Object.keys(REQUIRED_FIELDS).includes(v),
|
action: (v) => Object.keys(REQUIRED_FIELDS).includes(v),
|
||||||
owner: (v) => !v || /^[\w\-]+$/.test(v),
|
owner: (v) => !v || /^[\w\-]+$/.test(v),
|
||||||
|
|
@ -151,7 +155,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Paste helper ----------------------
|
// ---------------------- Paste + Submit helpers ----------------------
|
||||||
function getVisibleInputCandidate() {
|
function getVisibleInputCandidate() {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
'.ProseMirror#prompt-textarea',
|
'.ProseMirror#prompt-textarea',
|
||||||
|
|
@ -172,6 +176,58 @@
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findSendButton() {
|
||||||
|
const selectors = [
|
||||||
|
'button[data-testid="send-button"]',
|
||||||
|
'button[aria-label*="Send"]',
|
||||||
|
'button[aria-label*="send"]',
|
||||||
|
'button[aria-label*="Submit"]',
|
||||||
|
'button[aria-label*="submit"]',
|
||||||
|
'form button[type="submit"]'
|
||||||
|
];
|
||||||
|
for (const s of selectors) {
|
||||||
|
const btn = document.querySelector(s);
|
||||||
|
if (!btn) continue;
|
||||||
|
const style = window.getComputedStyle(btn);
|
||||||
|
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
|
||||||
|
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
||||||
|
if (btn.offsetParent === null && style.position !== 'fixed') continue;
|
||||||
|
if (!disabled) return btn;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pressEnterOn(el) {
|
||||||
|
const events = ['keydown','keypress','keyup'];
|
||||||
|
for (const type of events) {
|
||||||
|
const ok = el.dispatchEvent(new KeyboardEvent(type, {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
keyCode: 13,
|
||||||
|
which: 13,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
}));
|
||||||
|
if (!ok) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitComposer() {
|
||||||
|
try {
|
||||||
|
const btn = findSendButton();
|
||||||
|
if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) {
|
||||||
|
btn.click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = getVisibleInputCandidate();
|
||||||
|
if (!el) return false;
|
||||||
|
return pressEnterOn(el);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function pasteToComposer(text) {
|
function pasteToComposer(text) {
|
||||||
try {
|
try {
|
||||||
const el = getVisibleInputCandidate();
|
const el = getVisibleInputCandidate();
|
||||||
|
|
@ -180,17 +236,19 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payload = CONFIG.APPEND_TRAILING_NEWLINE ? (text.endsWith('\n') ? text : text + '\n') : text;
|
||||||
|
|
||||||
el.focus();
|
el.focus();
|
||||||
|
|
||||||
// 1) Try a real-ish paste event with DataTransfer
|
// 1) ClipboardEvent paste
|
||||||
try {
|
try {
|
||||||
const dt = new DataTransfer();
|
const dt = new DataTransfer();
|
||||||
dt.setData('text/plain', text);
|
dt.setData('text/plain', payload);
|
||||||
const pasteEvt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
|
const pasteEvt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
|
||||||
if (el.dispatchEvent(pasteEvt) && !pasteEvt.defaultPrevented) return true;
|
if (el.dispatchEvent(pasteEvt) && !pasteEvt.defaultPrevented) return true;
|
||||||
} catch (_) { /* continue */ }
|
} catch (_) { /* continue */ }
|
||||||
|
|
||||||
// 2) Try execCommand insertText (still widely supported)
|
// 2) execCommand insertText
|
||||||
try {
|
try {
|
||||||
const sel = window.getSelection && window.getSelection();
|
const sel = window.getSelection && window.getSelection();
|
||||||
if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) {
|
if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) {
|
||||||
|
|
@ -200,14 +258,14 @@
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
sel.addRange(r);
|
sel.addRange(r);
|
||||||
}
|
}
|
||||||
if (document.execCommand && document.execCommand('insertText', false, text)) return true;
|
if (document.execCommand && document.execCommand('insertText', false, payload)) return true;
|
||||||
} catch (_) { /* continue */ }
|
} catch (_) { /* continue */ }
|
||||||
|
|
||||||
// 3) ProseMirror: inject paragraph HTML + notify input/change
|
// 3) ProseMirror innerHTML
|
||||||
const isPM = el.classList && el.classList.contains('ProseMirror');
|
const isPM = el.classList && el.classList.contains('ProseMirror');
|
||||||
if (isPM) {
|
if (isPM) {
|
||||||
const escape = (s) => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
const escape = (s) => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
const html = String(text).split('\n').map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>').join('');
|
const html = String(payload).split('\n').map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>').join('');
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
@ -216,20 +274,20 @@
|
||||||
|
|
||||||
// 4) contenteditable/textarea fallback
|
// 4) contenteditable/textarea fallback
|
||||||
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
||||||
el.value = text;
|
el.value = payload;
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
|
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
|
||||||
el.textContent = text;
|
el.textContent = payload;
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Clipboard fallback for manual paste
|
// 5) Clipboard fallback
|
||||||
try {
|
try {
|
||||||
if (typeof GM_setClipboard === 'function') {
|
if (typeof GM_setClipboard === 'function') {
|
||||||
GM_setClipboard(text, { type: 'text', mimetype: 'text/plain' });
|
GM_setClipboard(payload, { type: 'text', mimetype: 'text/plain' });
|
||||||
GM_notification({ title: 'AI Repo Commander', text: 'Content copied to clipboard — press Ctrl/Cmd+V to paste.', timeout: 5000 });
|
GM_notification({ title: 'AI Repo Commander', text: 'Content copied to clipboard — press Ctrl/Cmd+V to paste.', timeout: 5000 });
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
@ -241,6 +299,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pasteAndMaybeSubmit(text) {
|
||||||
|
const pasted = pasteToComposer(text);
|
||||||
|
if (!pasted) return false;
|
||||||
|
if (!CONFIG.AUTO_SUBMIT) return true;
|
||||||
|
await ExecutionManager.delay(CONFIG.POST_PASTE_DELAY_MS);
|
||||||
|
const ok = await submitComposer();
|
||||||
|
if (!ok) {
|
||||||
|
GM_notification({ title: 'AI Repo Commander', text: 'Pasted content, but auto-submit did not trigger.', timeout: 4000 });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------- Parser ----------------------
|
// ---------------------- Parser ----------------------
|
||||||
class CommandParser {
|
class CommandParser {
|
||||||
static parseYAMLCommand(codeBlockText) {
|
static parseYAMLCommand(codeBlockText) {
|
||||||
|
|
@ -262,14 +332,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
static extractCommandBlock(text) {
|
static extractCommandBlock(text) {
|
||||||
// We are passed the *code block text*. Accept ^%$bridge until explicit '---' or end-of-code-block (EOF).
|
// Find ^%$bridge anywhere in the code block, prefer explicit '---' terminator
|
||||||
// More flexible: allow ^%$bridge anywhere on its own line (with optional leading spaces).
|
const patterns = [
|
||||||
const start = text.search(/(^|\n)\s*\^%\$bridge\b/m);
|
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m,
|
||||||
if (start === -1) return null;
|
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)(?=\n\s*$|\n---|\n```|$)/m,
|
||||||
|
/^\s*\^%\$bridge[ \t]*\n([\s\S]*)/m
|
||||||
const after = text.slice(start);
|
];
|
||||||
const m = after.match(/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)(?:\n---[ \t]*(?:\n|$)|$)/m);
|
for (const pattern of patterns) {
|
||||||
return m ? m[1].trimEnd() : null;
|
const match = text.match(pattern);
|
||||||
|
if (match && match[1]?.trim()) return match[1].trimEnd();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseKeyValuePairs(block) {
|
static parseKeyValuePairs(block) {
|
||||||
|
|
@ -319,12 +392,9 @@
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const action = parsed.action;
|
const action = parsed.action;
|
||||||
if (!action) { errors.push('Missing required field: action'); return { isValid:false, errors }; }
|
if (!action) { errors.push('Missing required field: action'); return { isValid:false, errors }; }
|
||||||
|
|
||||||
const req = REQUIRED_FIELDS[action];
|
const req = REQUIRED_FIELDS[action];
|
||||||
if (!req) { errors.push(`Unknown action: ${action}`); return { isValid:false, errors }; }
|
if (!req) { errors.push(`Unknown action: ${action}`); return { isValid:false, errors }; }
|
||||||
|
|
||||||
for (const f of req) if (!parsed[f] && parsed[f] !== '') errors.push(`Missing required field: ${f}`);
|
for (const f of req) if (!parsed[f] && parsed[f] !== '') errors.push(`Missing required field: ${f}`);
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(parsed)) {
|
for (const [field, value] of Object.entries(parsed)) {
|
||||||
const validator = FIELD_VALIDATORS[field];
|
const validator = FIELD_VALIDATORS[field];
|
||||||
if (validator && !validator(value)) errors.push(`Invalid format for field: ${field}`);
|
if (validator && !validator(value)) errors.push(`Invalid format for field: ${field}`);
|
||||||
|
|
@ -337,7 +407,6 @@
|
||||||
class ExecutionManager {
|
class ExecutionManager {
|
||||||
static async executeCommand(command, sourceElement) {
|
static async executeCommand(command, sourceElement) {
|
||||||
try {
|
try {
|
||||||
// synthesize commit_message for file writes
|
|
||||||
if ((command.action === 'update_file' || command.action === 'create_file') && !command.commit_message) {
|
if ((command.action === 'update_file' || command.action === 'create_file') && !command.commit_message) {
|
||||||
command.commit_message = `AI Repo Commander: ${command.path} (${new Date().toISOString()})`;
|
command.commit_message = `AI Repo Commander: ${command.path} (${new Date().toISOString()})`;
|
||||||
}
|
}
|
||||||
|
|
@ -387,7 +456,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
static async mockExecution(command, sourceElement) {
|
static async mockExecution(command, sourceElement) {
|
||||||
await this.delay(1000); // consistency
|
await this.delay(1000);
|
||||||
const mock = {
|
const mock = {
|
||||||
status: 200,
|
status: 200,
|
||||||
responseText: JSON.stringify({
|
responseText: JSON.stringify({
|
||||||
|
|
@ -399,7 +468,50 @@
|
||||||
return this.handleSuccess(mock, command, sourceElement, true);
|
return this.handleSuccess(mock, command, sourceElement, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static handleSuccess(response, command, sourceElement, isMock = false) {
|
static _extractGetFileBody(payload) {
|
||||||
|
const item = Array.isArray(payload) ? payload[0] : payload;
|
||||||
|
return (
|
||||||
|
item?.result?.content?.data ??
|
||||||
|
item?.content?.data ??
|
||||||
|
payload?.result?.content?.data ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
if (Array.isArray(v) && v.length && (k.toLowerCase().includes('file') || typeof v[0] === 'string' || v[0]?.path || v[0]?.name)) {
|
||||||
|
files = v; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.isArray(files) ? files : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (f?.dir && f?.name) return `${f.dir.replace(/\/+$/,'')}/${f.name}`;
|
||||||
|
if (f?.name) return f.name;
|
||||||
|
try { return JSON.stringify(f); } catch { return String(f); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = files.map(pickPath).filter(Boolean).sort();
|
||||||
|
// Friendly text block (fits most chat UIs)
|
||||||
|
return '```text\n' + lines.join('\n') + '\n```';
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleSuccess(response, command, sourceElement, isMock = false) {
|
||||||
let data;
|
let data;
|
||||||
try { data = JSON.parse(response.responseText || '{}'); }
|
try { data = JSON.parse(response.responseText || '{}'); }
|
||||||
catch { data = { message: 'Operation completed (no JSON body)' }; }
|
catch { data = { message: 'Operation completed (no JSON body)' }; }
|
||||||
|
|
@ -409,28 +521,29 @@
|
||||||
details: data.message || 'Operation completed successfully'
|
details: data.message || 'Operation completed successfully'
|
||||||
});
|
});
|
||||||
|
|
||||||
// If this was a get_file, try to paste the returned content into the composer
|
// Auto-paste handlers
|
||||||
if (command.action === 'get_file') {
|
if (command.action === 'get_file') {
|
||||||
const payload = data;
|
const body = this._extractGetFileBody(data);
|
||||||
const item = Array.isArray(payload) ? payload[0] : payload;
|
|
||||||
|
|
||||||
// Try common shapes (n8n variations)
|
|
||||||
const body =
|
|
||||||
item?.result?.content?.data ??
|
|
||||||
item?.content?.data ??
|
|
||||||
payload?.result?.content?.data ??
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (typeof body === 'string' && body.length) {
|
if (typeof body === 'string' && body.length) {
|
||||||
const pasted = pasteToComposer(body);
|
await pasteAndMaybeSubmit(body);
|
||||||
if (!pasted) {
|
|
||||||
GM_notification({ title: 'AI Repo Commander', text: 'Fetched file (get_file), but could not paste into input.', timeout: 4000 });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log('[AI Repo Commander] get_file: no content.data in response', data);
|
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
|
||||||
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but response had no content.data', timeout: 4000 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.action === 'list_files') {
|
||||||
|
const files = this._extractFilesArray(data);
|
||||||
|
if (files && files.length) {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, data, isMock };
|
return { success: true, data, isMock };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,7 +559,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Bridge Key ----------------------
|
// ---------------------- Bridge Key ----------------------
|
||||||
let BRIDGE_KEY = null; // (Declared near top for clarity)
|
let BRIDGE_KEY = null;
|
||||||
function requireBridgeKeyIfNeeded() {
|
function requireBridgeKeyIfNeeded() {
|
||||||
if (CONFIG.ENABLE_API && !BRIDGE_KEY) {
|
if (CONFIG.ENABLE_API && !BRIDGE_KEY) {
|
||||||
BRIDGE_KEY = prompt('[AI Repo Commander] Enter your bridge key for this session:');
|
BRIDGE_KEY = prompt('[AI Repo Commander] Enter your bridge key for this session:');
|
||||||
|
|
@ -497,7 +610,6 @@
|
||||||
this.scanMessages();
|
this.scanMessages();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
// Optional deeper scan if explicitly enabled
|
|
||||||
if (CONFIG.PROCESS_EXISTING) {
|
if (CONFIG.PROCESS_EXISTING) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.log('Deep scan of existing messages (PROCESS_EXISTING=true)');
|
this.log('Deep scan of existing messages (PROCESS_EXISTING=true)');
|
||||||
|
|
@ -506,43 +618,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scanNode(node) {
|
scanNode() { this.scanMessages(); }
|
||||||
if (node.querySelector) this.scanMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
scanExistingMessages() { setTimeout(() => this.scanMessages(), 1000); }
|
|
||||||
|
|
||||||
isAssistantMessage(el) {
|
isAssistantMessage(el) {
|
||||||
if (!CONFIG.ASSISTANT_ONLY) return true; // Process all messages
|
if (!CONFIG.ASSISTANT_ONLY) return true;
|
||||||
|
|
||||||
const host = location.hostname;
|
const host = location.hostname;
|
||||||
|
|
||||||
// OpenAI/ChatGPT: reliable role attribute
|
|
||||||
if (/chat\.openai\.com|chatgpt\.com/.test(host)) {
|
if (/chat\.openai\.com|chatgpt\.com/.test(host)) {
|
||||||
return !!el.closest?.('[data-message-author-role="assistant"]');
|
return !!el.closest?.('[data-message-author-role="assistant"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claude: prefer explicit role if present; else allow (Claude DOM varies)
|
|
||||||
if (/claude\.ai/.test(host)) {
|
if (/claude\.ai/.test(host)) {
|
||||||
const isUser = !!el.closest?.('[data-message-author-role="user"]');
|
const isUser = !!el.closest?.('[data-message-author-role="user"]');
|
||||||
return !isUser;
|
return !isUser;
|
||||||
}
|
}
|
||||||
|
if (/gemini\.google\.com/.test(host)) return true;
|
||||||
// Gemini: DOM varies; allow by default (can refine later)
|
|
||||||
if (/gemini\.google\.com/.test(host)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown platforms: allow
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require: command must be in a code block (pre/code)
|
|
||||||
findCommandInCodeBlock(el) {
|
findCommandInCodeBlock(el) {
|
||||||
const blocks = el.querySelectorAll('pre code, pre, code');
|
const blocks = el.querySelectorAll('pre code, pre, code');
|
||||||
for (const b of blocks) {
|
for (const b of blocks) {
|
||||||
const txt = (b.textContent || '').trim();
|
const txt = (b.textContent || '').trim();
|
||||||
// Flexible: allow ^%$bridge to appear anywhere on its own line
|
|
||||||
if (/(^|\n)\s*\^%\$bridge\b/m.test(txt)) {
|
if (/(^|\n)\s*\^%\$bridge\b/m.test(txt)) {
|
||||||
return { blockElement: b, text: txt };
|
return { blockElement: b, text: txt };
|
||||||
}
|
}
|
||||||
|
|
@ -573,7 +668,6 @@
|
||||||
|
|
||||||
const cmdText = hit.text;
|
const cmdText = hit.text;
|
||||||
|
|
||||||
// Persistent dedupe: skip if we've already executed this exact block recently
|
|
||||||
if (this.history.has(cmdText)) {
|
if (this.history.has(cmdText)) {
|
||||||
this.log('Skipping already-executed command (persistent history)');
|
this.log('Skipping already-executed command (persistent history)');
|
||||||
return;
|
return;
|
||||||
|
|
@ -617,17 +711,14 @@
|
||||||
const before = message.originalText;
|
const before = message.originalText;
|
||||||
await this.debounce();
|
await this.debounce();
|
||||||
|
|
||||||
// Re-extract after debounce in case the AI finished streaming or edited
|
|
||||||
const after = this.reextractCommandText(message.element);
|
const after = this.reextractCommandText(message.element);
|
||||||
|
|
||||||
// If command disappeared, abort gracefully
|
|
||||||
if (!after) {
|
if (!after) {
|
||||||
this.log('Command removed during debounce - aborting');
|
this.log('Command removed during debounce - aborting');
|
||||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If changed, re-debounce once and re-parse
|
|
||||||
if (after !== before) {
|
if (after !== before) {
|
||||||
this.log('Command changed during debounce - updating and re-debouncing once');
|
this.log('Command changed during debounce - updating and re-debouncing once');
|
||||||
message.originalText = after;
|
message.originalText = after;
|
||||||
|
|
@ -648,9 +739,7 @@
|
||||||
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
||||||
await ExecutionManager.executeCommand(parsed, message.element);
|
await ExecutionManager.executeCommand(parsed, message.element);
|
||||||
|
|
||||||
// Mark the FINAL executed version in history
|
|
||||||
this.history.mark(message.originalText);
|
this.history.mark(message.originalText);
|
||||||
|
|
||||||
this.updateState(messageId, COMMAND_STATES.COMPLETE);
|
this.updateState(messageId, COMMAND_STATES.COMPLETE);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -716,6 +805,16 @@ repo: test-repo
|
||||||
path: README.md
|
path: README.md
|
||||||
---
|
---
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
`,
|
||||||
|
listFiles:
|
||||||
|
`\
|
||||||
|
\`\`\`yaml
|
||||||
|
^%$bridge
|
||||||
|
action: list_files
|
||||||
|
repo: test-repo
|
||||||
|
path: .
|
||||||
|
---
|
||||||
|
\`\`\`
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -729,7 +828,8 @@ path: README.md
|
||||||
config: CONFIG,
|
config: CONFIG,
|
||||||
test: TEST_COMMANDS,
|
test: TEST_COMMANDS,
|
||||||
version: CONFIG.VERSION,
|
version: CONFIG.VERSION,
|
||||||
history: commandMonitor.history
|
history: commandMonitor.history,
|
||||||
|
submitComposer // expose for quick testing
|
||||||
};
|
};
|
||||||
console.log('AI Repo Commander fully initialized');
|
console.log('AI Repo Commander fully initialized');
|
||||||
console.log('API Enabled:', CONFIG.ENABLE_API);
|
console.log('API Enabled:', CONFIG.ENABLE_API);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue