// ==COMMAND PARSER START== // Module: command-parser.js // Purpose: Extract and parse YAML-like command blocks embedded in assistant messages. // - Looks for a complete block delimited by @bridge@ ... @end@ // - Parses simple key/value pairs and multiline "|" blocks // - Applies sane defaults (url, owner, source_branch) // - Validates presence of required fields per action // This module is side-effect free and exposes a single class via window.AI_REPO_PARSER. (function () { /** * CommandParser * - parse(text): Extracts first complete command block and returns a structured object * - extractBlock(text): Returns the inner text between @bridge@ and @end@ * - parseKV(block): Minimal YAML-like parser supporting multi-line values with "|" * - applyDefaults(obj): Applies default values and owner/repo split logic * - validate(obj): Returns { isValid, errors, example? } */ class CommandParser { static REQUIRED = { get_file: ['action', 'repo', 'path'], update_file: ['action', 'repo', 'path', 'content'], create_file: ['action', 'repo', 'path', 'content'], create_repo: ['action', 'repo'], create_branch:['action', 'repo', 'branch'], create_pr: ['action', 'repo', 'title', 'head', 'base'], list_files: ['action', 'repo', 'path'] }; static parse(text) { const block = this.extractBlock(text); if (!block) throw new Error('No complete @bridge@ command found (missing @end@)'); const parsed = this.parseKV(block); this.applyDefaults(parsed); return parsed; } static extractBlock(text) { const m = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/m.exec(text); return m?.[1]?.trim() || null; } // Simple YAML-like parser (supports "key: value" & "key: |" multiline) static parseKV(block) { const out = {}; const lines = block.split('\n'); let curKey = null, multi = false, buf = []; const flush = () => { if (multi && curKey) out[curKey] = buf.join('\n').replace(/\s+$/,''); curKey = null; buf = []; multi = false; }; for (const raw of lines) { const line = raw.replace(/\r$/, ''); if (multi) { // End multiline if we see an unindented key pattern if (/^[A-Za-z_][\w\-]*\s*:/.test(line) && !/^\s/.test(line)) { flush(); } else { buf.push(line); continue; } } const idx = line.indexOf(':'); if (idx !== -1) { const key = line.slice(0, idx).trim(); let value = line.slice(idx + 1).trim(); if (value === '|') { curKey = key; multi = true; buf = []; } else { out[key] = value; curKey = key; } } else if (multi) { buf.push(line); } } flush(); return out; } static applyDefaults(p) { p.url = p.url || 'https://n8n.brrd.tech/webhook/ai-gitea-bridge'; p.owner = p.owner || 'rob'; if (p.action === 'create_branch' && !p.source_branch) p.source_branch = 'main'; if (typeof p.repo === 'string' && p.repo.includes('/')) { const [owner, repo] = p.repo.split('/', 2); if (!p.owner) p.owner = owner; p.repo = repo; } } static validate(p) { const errors = []; // explicit example flag if (p.example === true || String(p.example).toLowerCase() === 'true' || String(p.example).toLowerCase() === 'yes') { return { isValid: true, errors: [], example: true }; } const action = p.action; if (!action) return { isValid: false, errors: ['Missing required field: action'] }; const req = this.REQUIRED[action]; if (!req) return { isValid: false, errors: [`Unknown action: ${action}`] }; for (const f of req) if (!(f in p) || p[f] === '') errors.push(`Missing required field: ${f}`); return { isValid: errors.length === 0, errors }; } } window.AI_REPO_PARSER = CommandParser; })(); // ==COMMAND PARSER END==