114 lines
4.5 KiB
JavaScript
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==
|