Update src/ai-repo-commander.user.js
1. Fixed YAML Parsing Edge Cases Multi-line content with colons now works correctly Uses TOP_LEVEL_KEYS to only stop collection on known keys Much more robust for real-world content 2. Better Command Block Extraction More tolerant --- matching (allows trailing spaces) Won't break on content that happens to contain --- 3. Cleaner Owner/Repo Handling Moved normalization to parser (side-effect free validation) More predictable behavior 4. Improved Error Messaging Clear retry attempt counts: "failed after 2 attempts; max 3" Better user feedback 5. Robust JSON Parsing Handles malformed API responses gracefully Won't crash on non-JSON responses 6. Better Message ID Generation Added timestamp and random salt to prevent collisions Uses data attributes to persist IDs 7. User Experience Bridge key warning on initialization Cleaner separation of concerns
This commit is contained in:
parent
d53162e021
commit
ebd72ed0c9
|
|
@ -1,8 +1,8 @@
|
|||
// ==UserScript==
|
||||
// @name AI Repo Commander
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 1.0.1
|
||||
// @description Enable AI assistants to securely interact with git repositories via YAML commands
|
||||
// @version 1.0.2
|
||||
// @description Enable AI assistants to securely interact with git repositories via YAML commands, with safety-first execution and clear inline feedback.
|
||||
// @author Your Name
|
||||
// @match https://chat.openai.com/*
|
||||
// @match https://claude.ai/*
|
||||
|
|
@ -17,11 +17,11 @@
|
|||
|
||||
// Configuration - MUST be manually enabled for production
|
||||
const CONFIG = {
|
||||
ENABLE_API: false, // Master kill switch
|
||||
DEBUG_MODE: true, // Development logging
|
||||
DEBOUNCE_DELAY: 5000, // 5-second bot typing protection
|
||||
MAX_RETRIES: 2, // API retry attempts
|
||||
VERSION: '1.0.1'
|
||||
ENABLE_API: false, // Master kill switch
|
||||
DEBUG_MODE: true, // Development logging
|
||||
DEBOUNCE_DELAY: 5000, // 5-second bot typing protection
|
||||
MAX_RETRIES: 2, // API retry attempts (2 retries => up to 3 total attempts)
|
||||
VERSION: '1.0.2'
|
||||
};
|
||||
|
||||
// Platform-specific DOM selectors
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
// Core Monitor Class
|
||||
class CommandMonitor {
|
||||
constructor() {
|
||||
this.trackedMessages = new Map();
|
||||
this.trackedMessages = new Map(); // id -> { element, originalText, state, ... }
|
||||
this.observer = null;
|
||||
this.currentPlatform = null;
|
||||
this.initialize();
|
||||
|
|
@ -108,6 +108,9 @@
|
|||
this.startObservation();
|
||||
this.setupEmergencyStop();
|
||||
this.log('AI Repo Commander initialized', CONFIG);
|
||||
if (CONFIG.ENABLE_API) {
|
||||
console.warn('[AI Repo Commander] API is enabled — you will be prompted for your bridge key on first command.');
|
||||
}
|
||||
}
|
||||
|
||||
detectPlatform() {
|
||||
|
|
@ -119,17 +122,16 @@
|
|||
this.observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === 1) { // Element node
|
||||
if (node.nodeType === 1) { // Element
|
||||
this.scanNode(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const targetNode = document.body;
|
||||
this.observer.observe(targetNode, { childList: true, subtree: true });
|
||||
this.observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Initial scan of existing messages
|
||||
// Initial scan
|
||||
this.scanExistingMessages();
|
||||
}
|
||||
|
||||
|
|
@ -156,8 +158,12 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Stable id: persist a data attribute to avoid duplicate processing on rescans
|
||||
getMessageId(element) {
|
||||
return element.id || element.className + '-' + Array.from(element.children).length;
|
||||
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;
|
||||
}
|
||||
|
||||
extractText(element) {
|
||||
|
|
@ -265,7 +271,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Command Parser Class (sturdier block extraction + simple content: |)
|
||||
// Command Parser Class (sturdier block extraction + simple content: | handling)
|
||||
class CommandParser {
|
||||
static parseYAMLCommand(text) {
|
||||
try {
|
||||
|
|
@ -273,10 +279,17 @@
|
|||
if (!commandBlock) throw new Error('No valid command block found');
|
||||
const parsed = this.parseKeyValuePairs(commandBlock);
|
||||
|
||||
// Set defaults
|
||||
// Defaults
|
||||
parsed.url = parsed.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge';
|
||||
parsed.owner = parsed.owner || 'brrd';
|
||||
|
||||
// Expand owner/repo shorthand here (not in validate)
|
||||
if (parsed.repo && typeof parsed.repo === 'string' && parsed.repo.includes('/')) {
|
||||
const [owner, repo] = parsed.repo.split('/', 2);
|
||||
if (!parsed.owner) parsed.owner = owner;
|
||||
parsed.repo = repo;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
throw new Error(`YAML parsing failed: ${error.message}`);
|
||||
|
|
@ -284,8 +297,8 @@
|
|||
}
|
||||
|
||||
static extractCommandBlock(text) {
|
||||
// Finds the first ^%$bridge block up to the next line that contains only ---
|
||||
const m = text.match(/^\^%\$bridge[ \t]*\n([\s\S]*?)\n---\s*$/m);
|
||||
// Find ^%$bridge … up to a line with --- (allow trailing spaces) then newline OR end of string
|
||||
const m = text.match(/^\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m);
|
||||
return m ? m[1].trimEnd() : null;
|
||||
}
|
||||
|
||||
|
|
@ -295,13 +308,18 @@
|
|||
let currentKey = null;
|
||||
let collecting = false;
|
||||
let buf = [];
|
||||
const TOP_LEVEL_KEYS = ['action','repo','path','content','owner','url','commit_message','branch','ref'];
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.replace(/\r$/, '');
|
||||
|
||||
if (collecting) {
|
||||
// stop if we hit an unindented key:value (loose heuristic)
|
||||
if (/^[A-Za-z_][\w\-]*\s*:/.test(line)) {
|
||||
// Only stop if UNINDENTED and a KNOWN top-level key
|
||||
const looksKey = /^[A-Za-z_][\w\-]*\s*:/.test(line);
|
||||
const unindented = !/^[ \t]/.test(line);
|
||||
const isTopKey = looksKey && unindented &&
|
||||
TOP_LEVEL_KEYS.some(k => line.startsWith(k + ':'));
|
||||
if (isTopKey) {
|
||||
result[currentKey] = buf.join('\n').trimEnd();
|
||||
collecting = false;
|
||||
buf = [];
|
||||
|
|
@ -343,13 +361,6 @@
|
|||
static validateStructure(parsed) {
|
||||
const errors = [];
|
||||
|
||||
// Expand owner/repo shorthand if present
|
||||
if (parsed.repo && parsed.repo.includes('/')) {
|
||||
const [owner, repo] = parsed.repo.split('/', 2);
|
||||
parsed.owner = parsed.owner || owner;
|
||||
parsed.repo = repo;
|
||||
}
|
||||
|
||||
// Required fields per action
|
||||
const action = parsed.action;
|
||||
if (!action) {
|
||||
|
|
@ -369,7 +380,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Field format validators
|
||||
// Field format validators (includes path traversal guard)
|
||||
for (const [field, value] of Object.entries(parsed)) {
|
||||
const validator = FIELD_VALIDATORS[field];
|
||||
if (validator && !validator(value)) {
|
||||
|
|
@ -377,11 +388,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Minimal traversal guard (remove if you truly don't want it)
|
||||
if (parsed.path && (parsed.path.startsWith('/') || parsed.path.includes('..') || parsed.path.includes('\\'))) {
|
||||
errors.push('Invalid path: no leading "/" or ".." or "\\" allowed.');
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors };
|
||||
}
|
||||
}
|
||||
|
|
@ -413,10 +419,11 @@
|
|||
return await this.makeAPICall(command);
|
||||
} catch (error) {
|
||||
if (attempt < CONFIG.MAX_RETRIES) {
|
||||
await this.delay(1000 * (attempt + 1)); // simple backoff: 1s, 2s
|
||||
await this.delay(1000 * (attempt + 1)); // simple backoff: 1s, 2s, ...
|
||||
return this.makeAPICallWithRetry(command, attempt + 1);
|
||||
}
|
||||
throw new Error(`${error.message} (after ${attempt + 1} attempts)`);
|
||||
const totalAttempts = attempt + 1; // attempts actually made
|
||||
throw new Error(`${error.message} (failed after ${totalAttempts} attempts; max ${CONFIG.MAX_RETRIES + 1})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -464,7 +471,12 @@
|
|||
}
|
||||
|
||||
static handleSuccess(response, command, sourceElement, isMock = false) {
|
||||
const responseData = JSON.parse(response.responseText || '{}');
|
||||
let responseData;
|
||||
try {
|
||||
responseData = JSON.parse(response.responseText || '{}');
|
||||
} catch {
|
||||
responseData = { message: 'Operation completed (no JSON body)' };
|
||||
}
|
||||
const templateType = isMock ? 'MOCK' : 'SUCCESS';
|
||||
|
||||
UIFeedback.appendStatus(sourceElement, templateType, {
|
||||
|
|
@ -522,11 +534,11 @@
|
|||
|
||||
static colorCodeStatus(type) {
|
||||
const colors = {
|
||||
'SUCCESS': '#10B981', // Green
|
||||
'ERROR': '#EF4444', // Red
|
||||
'SUCCESS': '#10B981', // Green
|
||||
'ERROR': '#EF4444', // Red
|
||||
'VALIDATION_ERROR': '#F59E0B', // Yellow
|
||||
'EXECUTING': '#3B82F6', // Blue
|
||||
'MOCK': '#8B5CF6' // Purple
|
||||
'EXECUTING': '#3B82F6', // Blue
|
||||
'MOCK': '#8B5CF6' // Purple
|
||||
};
|
||||
return colors[type] || '#6B7280'; // Default gray
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue