AI-Repo-Commander/src/command-parser.js

114 lines
4.5 KiB
JavaScript

// ==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==