liftosaur-mcp / minimal-validator.js
davanstrien's picture
davanstrien HF Staff
Deploy Liftoscript MCP validator
d0808a4
#!/usr/bin/env node
// Minimal Liftoscript validator using Lezer parsers
// No dependencies on the full Liftosaur codebase
const { parser: liftoscriptParser } = require('./liftoscript-parser.js');
const { parser: plannerParser } = require('./planner-parser.js');
function getLineAndColumn(text, position) {
const lines = text.split('\n');
let currentPos = 0;
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length + 1; // +1 for newline
if (position < currentPos + lineLength) {
return {
line: i + 1,
column: position - currentPos + 1
};
}
currentPos += lineLength;
}
return { line: lines.length, column: 1 };
}
function detectScriptType(script) {
// Detect if this is planner syntax or pure Liftoscript
const plannerIndicators = [
/^#\s+Week/m,
/^##\s+Day/m,
/^\s*\w+\s*\/\s*\d+x\d+/m, // Exercise format like "Squat / 3x5"
/\/\s*progress:/,
/\/\s*warmup:/,
/\/\s*update:/
];
return plannerIndicators.some(regex => regex.test(script)) ? 'planner' : 'liftoscript';
}
function validateLiftoscript(script) {
try {
const scriptType = detectScriptType(script);
let tree;
if (scriptType === 'planner') {
// Parse with planner parser
tree = plannerParser.parse(script);
} else {
// Parse as pure Liftoscript
tree = liftoscriptParser.parse(script);
}
// Check for error nodes
let hasError = false;
let errorNode = null;
let errorType = null;
tree.iterate({
enter: (node) => {
if (node.type.isError) {
hasError = true;
errorNode = node;
errorType = node.type.name;
return false; // Stop iteration
}
}
});
if (hasError && errorNode) {
const { line, column } = getLineAndColumn(script, errorNode.from);
// Try to provide more helpful error messages
let message = `Syntax error`;
const problemText = script.substring(errorNode.from, Math.min(errorNode.to, errorNode.from + 20));
if (scriptType === 'planner') {
if (problemText.includes('/')) {
message = "Invalid exercise format. Expected: 'Exercise / Sets x Reps'";
} else if (problemText.includes(':')) {
message = "Invalid property format. Expected: 'property: value' or 'property: function(args)'";
}
} else {
if (problemText.includes('=')) {
message = "Invalid assignment. Check variable names and syntax";
} else if (problemText.includes('{') || problemText.includes('}')) {
message = "Unmatched braces";
}
}
return {
valid: false,
error: {
message: `${message} at "${problemText.trim()}"`,
line: line,
column: column,
type: scriptType
}
};
}
// No syntax errors found
return {
valid: true,
error: null,
type: scriptType
};
} catch (e) {
return {
valid: false,
error: {
message: `Parser error: ${e.message}`,
line: 0,
column: 0
}
};
}
}
// Export for use
module.exports = { validateLiftoscript };
// CLI interface
if (require.main === module) {
const fs = require('fs');
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Liftoscript Validator');
console.log('Usage: node minimal-validator.js <script or -> [--json]');
console.log('\nExamples:');
console.log(' node minimal-validator.js "state.weight = 100lb"');
console.log(' node minimal-validator.js - < program.liftoscript');
console.log(' echo "Squat / 3x5" | node minimal-validator.js - --json');
process.exit(0);
}
let script;
const jsonOutput = args.includes('--json');
if (args[0] === '-') {
// Read from stdin
script = fs.readFileSync(0, 'utf-8');
} else {
script = args[0];
}
const result = validateLiftoscript(script);
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.valid) {
console.log(`✅ Valid ${result.type} syntax`);
} else {
console.error(`❌ Invalid syntax (${result.error.type || 'unknown'} mode)`);
console.error(` Line ${result.error.line}, Column ${result.error.column}: ${result.error.message}`);
process.exit(1);
}
}
}