929 lines
32 KiB
JavaScript
929 lines
32 KiB
JavaScript
/**
|
|
* @claude-flow/codex - Validators
|
|
*
|
|
* Comprehensive validation functions for AGENTS.md, SKILL.md, and config.toml
|
|
* Provides detailed error messages and suggestions for fixes.
|
|
*/
|
|
/**
|
|
* Secret patterns to detect
|
|
*/
|
|
const SECRET_PATTERNS = [
|
|
{ pattern: /sk-[a-zA-Z0-9]{32,}/, name: 'OpenAI API key' },
|
|
{ pattern: /sk-ant-[a-zA-Z0-9-]{32,}/, name: 'Anthropic API key' },
|
|
{ pattern: /ghp_[a-zA-Z0-9]{36}/, name: 'GitHub personal access token' },
|
|
{ pattern: /gho_[a-zA-Z0-9]{36}/, name: 'GitHub OAuth token' },
|
|
{ pattern: /github_pat_[a-zA-Z0-9_]{22,}/, name: 'GitHub fine-grained token' },
|
|
{ pattern: /xox[baprs]-[a-zA-Z0-9-]{10,}/, name: 'Slack token' },
|
|
{ pattern: /AKIA[A-Z0-9]{16}/, name: 'AWS access key' },
|
|
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']?[a-zA-Z0-9_-]{20,}["']?/i, name: 'Generic API key' },
|
|
{ pattern: /(?:password|passwd|pwd)\s*[:=]\s*["'][^"']{8,}["']/i, name: 'Hardcoded password' },
|
|
{ pattern: /(?:secret|token)\s*[:=]\s*["'][a-zA-Z0-9_/-]{16,}["']/i, name: 'Hardcoded secret/token' },
|
|
{ pattern: /Bearer\s+[a-zA-Z0-9_.-]{20,}/, name: 'Bearer token' },
|
|
{ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/, name: 'Private key' },
|
|
];
|
|
/**
|
|
* Required sections for AGENTS.md
|
|
*/
|
|
const AGENTS_MD_REQUIRED_SECTIONS = ['Setup', 'Code Standards', 'Security'];
|
|
/**
|
|
* Recommended sections for AGENTS.md
|
|
*/
|
|
const AGENTS_MD_RECOMMENDED_SECTIONS = [
|
|
'Project Overview',
|
|
'Skills',
|
|
'Agent Types',
|
|
'Memory System',
|
|
'Links',
|
|
];
|
|
/**
|
|
* Valid approval policies
|
|
*/
|
|
const VALID_APPROVAL_POLICIES = ['untrusted', 'on-failure', 'on-request', 'never'];
|
|
/**
|
|
* Valid sandbox modes
|
|
*/
|
|
const VALID_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'];
|
|
/**
|
|
* Valid web search modes
|
|
*/
|
|
const VALID_WEB_SEARCH_MODES = ['disabled', 'cached', 'live'];
|
|
/**
|
|
* Required config.toml fields
|
|
*/
|
|
const CONFIG_TOML_REQUIRED_FIELDS = ['model', 'approval_policy', 'sandbox_mode'];
|
|
/**
|
|
* Validate an AGENTS.md file
|
|
*/
|
|
export async function validateAgentsMd(content) {
|
|
const errors = [];
|
|
const warnings = [];
|
|
const lines = content.split('\n');
|
|
// Check for title (H1 heading)
|
|
if (!content.startsWith('# ')) {
|
|
const firstHeadingMatch = content.match(/^(#{1,6})\s+/m);
|
|
if (firstHeadingMatch && firstHeadingMatch[1]) {
|
|
if (firstHeadingMatch[1].length > 1) {
|
|
errors.push({
|
|
path: 'AGENTS.md',
|
|
message: 'AGENTS.md should start with a level-1 heading (# Title)',
|
|
line: 1,
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
errors.push({
|
|
path: 'AGENTS.md',
|
|
message: 'AGENTS.md must start with a title heading',
|
|
line: 1,
|
|
});
|
|
}
|
|
}
|
|
// Check for empty content
|
|
if (content.trim().length < 50) {
|
|
errors.push({
|
|
path: 'AGENTS.md',
|
|
message: 'AGENTS.md content is too short - add meaningful instructions',
|
|
line: 1,
|
|
});
|
|
}
|
|
// Extract sections
|
|
const sections = extractSections(content);
|
|
const sectionTitles = sections.map((s) => s.title.toLowerCase());
|
|
// Check for required sections
|
|
for (const required of AGENTS_MD_REQUIRED_SECTIONS) {
|
|
const found = sectionTitles.some((t) => t.includes(required.toLowerCase()) || t === required.toLowerCase());
|
|
if (!found) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: `Missing recommended section: ## ${required}`,
|
|
suggestion: `Add a "## ${required}" section for better agent guidance`,
|
|
});
|
|
}
|
|
}
|
|
// Check for recommended sections
|
|
for (const recommended of AGENTS_MD_RECOMMENDED_SECTIONS) {
|
|
const found = sectionTitles.some((t) => t.includes(recommended.toLowerCase()) || t === recommended.toLowerCase());
|
|
if (!found) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: `Consider adding section: ## ${recommended}`,
|
|
suggestion: `A "${recommended}" section would improve agent understanding`,
|
|
});
|
|
}
|
|
}
|
|
// Check for hardcoded secrets
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
if (pattern.test(line)) {
|
|
errors.push({
|
|
path: 'AGENTS.md',
|
|
message: `Potential ${name} detected - never commit secrets`,
|
|
line: i + 1,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Check for skill references
|
|
const dollarSkillPattern = /\$([a-z][a-z0-9-]+)/g;
|
|
const slashSkillPattern = /\/([a-z][a-z0-9-]+)/g;
|
|
const dollarSkills = content.match(dollarSkillPattern) || [];
|
|
const slashSkills = content.match(slashSkillPattern) || [];
|
|
if (dollarSkills.length === 0 && slashSkills.length === 0) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: 'No skill references found',
|
|
suggestion: 'Add skill references using $skill-name syntax (Codex) or /skill-name (Claude Code)',
|
|
});
|
|
}
|
|
// Warn about slash syntax (Claude Code style)
|
|
if (slashSkills.length > 0 && dollarSkills.length === 0) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: 'Using Claude Code skill syntax (/skill-name)',
|
|
suggestion: 'Codex uses $skill-name syntax. Consider migrating for full compatibility.',
|
|
});
|
|
}
|
|
// Check for code blocks
|
|
const codeBlockCount = (content.match(/```/g) || []).length / 2;
|
|
if (codeBlockCount < 1) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: 'No code examples found',
|
|
suggestion: 'Add code examples in fenced code blocks (```) to guide agent behavior',
|
|
});
|
|
}
|
|
// Check for common issues
|
|
checkCommonIssues(content, lines, errors, warnings);
|
|
// Check structure
|
|
validateMarkdownStructure(content, lines, errors, warnings);
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
/**
|
|
* Validate a SKILL.md file
|
|
*/
|
|
export async function validateSkillMd(content) {
|
|
const errors = [];
|
|
const warnings = [];
|
|
const lines = content.split('\n');
|
|
// Check for YAML frontmatter
|
|
if (!content.startsWith('---')) {
|
|
errors.push({
|
|
path: 'SKILL.md',
|
|
message: 'SKILL.md must start with YAML frontmatter (---)',
|
|
line: 1,
|
|
});
|
|
return { valid: false, errors, warnings };
|
|
}
|
|
// Parse YAML frontmatter
|
|
const frontmatterResult = parseYamlFrontmatter(content);
|
|
if (!frontmatterResult.valid) {
|
|
for (const err of frontmatterResult.errors) {
|
|
errors.push({
|
|
path: 'SKILL.md',
|
|
message: err.message,
|
|
line: err.line,
|
|
});
|
|
}
|
|
return { valid: false, errors, warnings };
|
|
}
|
|
const frontmatter = frontmatterResult.data;
|
|
// Check required frontmatter fields
|
|
const requiredFields = ['name', 'description'];
|
|
for (const field of requiredFields) {
|
|
if (!(field in frontmatter)) {
|
|
errors.push({
|
|
path: 'SKILL.md',
|
|
message: `Missing required frontmatter field: ${field}`,
|
|
line: 2,
|
|
});
|
|
}
|
|
else if (typeof frontmatter[field] !== 'string' || frontmatter[field].trim() === '') {
|
|
errors.push({
|
|
path: 'SKILL.md',
|
|
message: `Field "${field}" must be a non-empty string`,
|
|
line: 2,
|
|
});
|
|
}
|
|
}
|
|
// Validate name format
|
|
if (frontmatter.name && typeof frontmatter.name === 'string') {
|
|
const name = frontmatter.name;
|
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
errors.push({
|
|
path: 'SKILL.md',
|
|
message: `Skill name "${name}" must be lowercase with hyphens only (e.g., my-skill)`,
|
|
line: 2,
|
|
});
|
|
}
|
|
if (name.length > 50) {
|
|
warnings.push({
|
|
path: 'SKILL.md',
|
|
message: 'Skill name is very long',
|
|
suggestion: 'Keep skill names under 50 characters for readability',
|
|
});
|
|
}
|
|
}
|
|
// Check optional but recommended fields
|
|
const recommendedFields = ['version', 'author', 'tags'];
|
|
for (const field of recommendedFields) {
|
|
if (!(field in frontmatter)) {
|
|
warnings.push({
|
|
path: 'SKILL.md',
|
|
message: `Consider adding field: ${field}`,
|
|
suggestion: `Adding "${field}" improves skill discoverability`,
|
|
});
|
|
}
|
|
}
|
|
// Check for model field (should specify min requirements)
|
|
if (frontmatter.model) {
|
|
warnings.push({
|
|
path: 'SKILL.md',
|
|
message: 'Model specification found in frontmatter',
|
|
suggestion: 'Model requirements are informational - skills work with any capable model',
|
|
});
|
|
}
|
|
// Get body content (after frontmatter)
|
|
const bodyStartLine = frontmatterResult.endLine + 1;
|
|
const body = lines.slice(bodyStartLine).join('\n');
|
|
// Check for Purpose section
|
|
if (!body.includes('## Purpose') && !body.includes('## Overview')) {
|
|
warnings.push({
|
|
path: 'SKILL.md',
|
|
message: 'Missing Purpose or Overview section',
|
|
suggestion: 'Add a "## Purpose" section to describe what the skill does',
|
|
});
|
|
}
|
|
// Check for trigger conditions
|
|
const hasTriggers = body.includes('When to Trigger') ||
|
|
body.includes('When to Use') ||
|
|
body.includes('Triggers') ||
|
|
(frontmatter.triggers && Array.isArray(frontmatter.triggers));
|
|
if (!hasTriggers) {
|
|
warnings.push({
|
|
path: 'SKILL.md',
|
|
message: 'Missing trigger conditions',
|
|
suggestion: 'Add a section or frontmatter field describing when to trigger this skill',
|
|
});
|
|
}
|
|
// Check for skip conditions
|
|
const hasSkipWhen = body.includes('Skip When') ||
|
|
body.includes('When to Skip') ||
|
|
(frontmatter.skip_when && Array.isArray(frontmatter.skip_when));
|
|
if (!hasSkipWhen) {
|
|
warnings.push({
|
|
path: 'SKILL.md',
|
|
message: 'No skip conditions defined',
|
|
suggestion: 'Consider adding skip conditions to prevent unnecessary skill invocation',
|
|
});
|
|
}
|
|
// Check for examples
|
|
const hasExamples = body.includes('## Example') || body.includes('```');
|
|
if (!hasExamples) {
|
|
warnings.push({
|
|
path: 'SKILL.md',
|
|
message: 'No examples provided',
|
|
suggestion: 'Add usage examples to help agents understand skill application',
|
|
});
|
|
}
|
|
// Check for secrets in content
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
if (pattern.test(line)) {
|
|
errors.push({
|
|
path: 'SKILL.md',
|
|
message: `Potential ${name} detected - never commit secrets`,
|
|
line: i + 1,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
/**
|
|
* Validate a config.toml file
|
|
*/
|
|
export async function validateConfigToml(content) {
|
|
const errors = [];
|
|
const warnings = [];
|
|
const lines = content.split('\n');
|
|
// Parse TOML
|
|
const parseResult = parseToml(content);
|
|
if (!parseResult.valid) {
|
|
for (const err of parseResult.errors) {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: err.message,
|
|
line: err.line,
|
|
});
|
|
}
|
|
return { valid: false, errors, warnings };
|
|
}
|
|
const config = parseResult.data;
|
|
// Check for required fields
|
|
for (const field of CONFIG_TOML_REQUIRED_FIELDS) {
|
|
const fieldLine = findFieldLine(lines, field);
|
|
if (!content.includes(`${field} =`) && !content.includes(`${field}=`)) {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: `Missing required field: ${field}`,
|
|
line: fieldLine,
|
|
});
|
|
}
|
|
}
|
|
// Validate model field
|
|
if (config.model) {
|
|
const model = config.model;
|
|
if (typeof model !== 'string') {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: 'model must be a string',
|
|
line: findFieldLine(lines, 'model'),
|
|
});
|
|
}
|
|
}
|
|
// Validate approval_policy value
|
|
const approvalMatch = content.match(/approval_policy\s*=\s*"([^"]+)"/);
|
|
if (approvalMatch) {
|
|
const policy = approvalMatch[1];
|
|
if (!VALID_APPROVAL_POLICIES.includes(policy)) {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: `Invalid approval_policy: "${policy}". Valid values: ${VALID_APPROVAL_POLICIES.join(', ')}`,
|
|
line: findFieldLine(lines, 'approval_policy'),
|
|
});
|
|
}
|
|
}
|
|
// Validate sandbox_mode value
|
|
const sandboxMatch = content.match(/sandbox_mode\s*=\s*"([^"]+)"/);
|
|
if (sandboxMatch) {
|
|
const mode = sandboxMatch[1];
|
|
if (!VALID_SANDBOX_MODES.includes(mode)) {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: `Invalid sandbox_mode: "${mode}". Valid values: ${VALID_SANDBOX_MODES.join(', ')}`,
|
|
line: findFieldLine(lines, 'sandbox_mode'),
|
|
});
|
|
}
|
|
}
|
|
// Validate web_search value
|
|
const webSearchMatch = content.match(/web_search\s*=\s*"([^"]+)"/);
|
|
if (webSearchMatch) {
|
|
const mode = webSearchMatch[1];
|
|
if (!VALID_WEB_SEARCH_MODES.includes(mode)) {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: `Invalid web_search: "${mode}". Valid values: ${VALID_WEB_SEARCH_MODES.join(', ')}`,
|
|
line: findFieldLine(lines, 'web_search'),
|
|
});
|
|
}
|
|
}
|
|
// Check for MCP servers section
|
|
if (!content.includes('[mcp_servers')) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: 'No MCP servers configured',
|
|
suggestion: 'Add [mcp_servers.claude-flow] for Claude Flow integration',
|
|
});
|
|
}
|
|
else {
|
|
// Validate MCP server configurations
|
|
validateMcpServers(content, lines, errors, warnings);
|
|
}
|
|
// Check for features section
|
|
if (!content.includes('[features]')) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: 'No [features] section found',
|
|
suggestion: 'Add [features] section to configure Codex behavior',
|
|
});
|
|
}
|
|
// Security warnings for dangerous settings
|
|
if (content.includes('approval_policy = "never"')) {
|
|
if (!content.includes('[profiles.')) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: 'Using "never" approval policy globally',
|
|
suggestion: 'Consider restricting to dev profile: [profiles.dev] approval_policy = "never"',
|
|
});
|
|
}
|
|
}
|
|
if (content.includes('sandbox_mode = "danger-full-access"')) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: 'Using "danger-full-access" sandbox mode',
|
|
suggestion: 'This gives unrestricted file system access. Use only in trusted environments.',
|
|
});
|
|
}
|
|
// Check for secrets
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
// Skip comment lines
|
|
if (line.trim().startsWith('#'))
|
|
continue;
|
|
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
if (pattern.test(line)) {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: `Potential ${name} detected - use environment variables instead`,
|
|
line: i + 1,
|
|
});
|
|
}
|
|
}
|
|
// Check for inline secrets in env sections
|
|
if (line.includes('_KEY =') || line.includes('_SECRET =') || line.includes('_TOKEN =')) {
|
|
const valueMatch = line.match(/=\s*"([^"]+)"/);
|
|
if (valueMatch && valueMatch[1] && !valueMatch[1].startsWith('$')) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: 'Hardcoded credential detected',
|
|
suggestion: `Use environment variable reference: $ENV_VAR_NAME instead of "${valueMatch[1]}"`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Validate project_doc_max_bytes if present
|
|
const maxBytesMatch = content.match(/project_doc_max_bytes\s*=\s*(\d+)/);
|
|
if (maxBytesMatch) {
|
|
const bytes = parseInt(maxBytesMatch[1], 10);
|
|
if (bytes < 1024) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: `project_doc_max_bytes is very low (${bytes} bytes)`,
|
|
suggestion: 'Consider increasing to at least 65536 for reasonable AGENTS.md support',
|
|
});
|
|
}
|
|
else if (bytes > 1048576) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: `project_doc_max_bytes is very high (${bytes} bytes = ${(bytes / 1024 / 1024).toFixed(1)} MB)`,
|
|
suggestion: 'Large values may impact performance. Default is 65536.',
|
|
});
|
|
}
|
|
}
|
|
// Check profiles
|
|
validateProfiles(content, lines, errors, warnings);
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
/**
|
|
* Validate all files in a project
|
|
*/
|
|
export async function validateProject(files) {
|
|
const results = {};
|
|
let totalErrors = 0;
|
|
let totalWarnings = 0;
|
|
if (files.agentsMd) {
|
|
results['AGENTS.md'] = await validateAgentsMd(files.agentsMd);
|
|
totalErrors += results['AGENTS.md'].errors.length;
|
|
totalWarnings += results['AGENTS.md'].warnings.length;
|
|
}
|
|
if (files.skillMds) {
|
|
for (const skill of files.skillMds) {
|
|
const key = `skills/${skill.name}`;
|
|
results[key] = await validateSkillMd(skill.content);
|
|
totalErrors += results[key].errors.length;
|
|
totalWarnings += results[key].warnings.length;
|
|
}
|
|
}
|
|
if (files.configToml) {
|
|
results['config.toml'] = await validateConfigToml(files.configToml);
|
|
totalErrors += results['config.toml'].errors.length;
|
|
totalWarnings += results['config.toml'].warnings.length;
|
|
}
|
|
return {
|
|
valid: totalErrors === 0,
|
|
results,
|
|
summary: { errors: totalErrors, warnings: totalWarnings },
|
|
};
|
|
}
|
|
/**
|
|
* Generate a validation report
|
|
*/
|
|
export function generateValidationReport(results) {
|
|
const lines = [];
|
|
lines.push('# Validation Report');
|
|
lines.push('');
|
|
let totalErrors = 0;
|
|
let totalWarnings = 0;
|
|
for (const [file, result] of Object.entries(results)) {
|
|
totalErrors += result.errors.length;
|
|
totalWarnings += result.warnings.length;
|
|
lines.push(`## ${file}`);
|
|
lines.push('');
|
|
lines.push(`**Status**: ${result.valid ? 'Valid' : 'Invalid'}`);
|
|
lines.push('');
|
|
if (result.errors.length > 0) {
|
|
lines.push('### Errors');
|
|
lines.push('');
|
|
for (const error of result.errors) {
|
|
const lineInfo = error.line ? ` (line ${error.line})` : '';
|
|
lines.push(`- ${error.message}${lineInfo}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
if (result.warnings.length > 0) {
|
|
lines.push('### Warnings');
|
|
lines.push('');
|
|
for (const warning of result.warnings) {
|
|
lines.push(`- ${warning.message}`);
|
|
if (warning.suggestion) {
|
|
lines.push(` - Suggestion: ${warning.suggestion}`);
|
|
}
|
|
}
|
|
lines.push('');
|
|
}
|
|
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
lines.push('No issues found.');
|
|
lines.push('');
|
|
}
|
|
}
|
|
lines.push('## Summary');
|
|
lines.push('');
|
|
lines.push(`- Total Errors: ${totalErrors}`);
|
|
lines.push(`- Total Warnings: ${totalWarnings}`);
|
|
lines.push(`- Overall Status: ${totalErrors === 0 ? 'PASS' : 'FAIL'}`);
|
|
lines.push('');
|
|
return lines.join('\n');
|
|
}
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
/**
|
|
* Extract sections from markdown content
|
|
*/
|
|
function extractSections(content) {
|
|
const sections = [];
|
|
const lines = content.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!line)
|
|
continue;
|
|
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
if (match && match[1] && match[2]) {
|
|
sections.push({
|
|
level: match[1].length,
|
|
title: match[2].trim(),
|
|
line: i + 1,
|
|
});
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
/**
|
|
* Parse YAML frontmatter
|
|
*/
|
|
function parseYamlFrontmatter(content) {
|
|
const result = {
|
|
valid: false,
|
|
errors: [],
|
|
data: {},
|
|
endLine: 0,
|
|
};
|
|
if (!content.startsWith('---')) {
|
|
result.errors.push({ line: 1, message: 'Missing opening ---' });
|
|
return result;
|
|
}
|
|
const lines = content.split('\n');
|
|
let endLineIndex = -1;
|
|
// Find closing ---
|
|
for (let i = 1; i < lines.length; i++) {
|
|
if (lines[i].trim() === '---') {
|
|
endLineIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (endLineIndex === -1) {
|
|
result.errors.push({ line: 1, message: 'YAML frontmatter not properly closed (missing closing ---)' });
|
|
return result;
|
|
}
|
|
result.endLine = endLineIndex;
|
|
// Parse YAML content (simple key: value parsing)
|
|
const yamlLines = lines.slice(1, endLineIndex);
|
|
for (let i = 0; i < yamlLines.length; i++) {
|
|
const line = yamlLines[i].trim();
|
|
if (line === '' || line.startsWith('#'))
|
|
continue;
|
|
// Simple key: value parsing
|
|
const colonIndex = line.indexOf(':');
|
|
if (colonIndex === -1) {
|
|
// Could be a list item or continuation
|
|
continue;
|
|
}
|
|
const key = line.substring(0, colonIndex).trim();
|
|
let value = line.substring(colonIndex + 1).trim();
|
|
// Parse value type
|
|
if (value === '') {
|
|
value = null;
|
|
}
|
|
else if (value === 'true') {
|
|
value = true;
|
|
}
|
|
else if (value === 'false') {
|
|
value = false;
|
|
}
|
|
else if (/^-?\d+$/.test(value)) {
|
|
value = parseInt(value, 10);
|
|
}
|
|
else if (/^-?\d+\.\d+$/.test(value)) {
|
|
value = parseFloat(value);
|
|
}
|
|
else if (value.startsWith('"') && value.endsWith('"')) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
else if (value.startsWith("'") && value.endsWith("'")) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
else if (value.startsWith('[') && value.endsWith(']')) {
|
|
// Simple inline array
|
|
try {
|
|
value = JSON.parse(value.replace(/'/g, '"'));
|
|
}
|
|
catch {
|
|
// Keep as string if not valid JSON
|
|
}
|
|
}
|
|
if (key) {
|
|
result.data[key] = value;
|
|
}
|
|
}
|
|
result.valid = true;
|
|
return result;
|
|
}
|
|
/**
|
|
* Parse TOML content (simplified parser)
|
|
*/
|
|
function parseToml(content) {
|
|
const result = {
|
|
valid: true,
|
|
errors: [],
|
|
data: {},
|
|
};
|
|
const lines = content.split('\n');
|
|
let currentSection = '';
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
// Skip empty lines and comments
|
|
if (line === '' || line.startsWith('#'))
|
|
continue;
|
|
// Section header
|
|
if (line.startsWith('[')) {
|
|
if (!line.endsWith(']')) {
|
|
result.errors.push({
|
|
line: i + 1,
|
|
message: `Invalid section header: ${line} (missing closing bracket)`,
|
|
});
|
|
result.valid = false;
|
|
continue;
|
|
}
|
|
currentSection = line.slice(1, -1);
|
|
continue;
|
|
}
|
|
// Key = value
|
|
const eqIndex = line.indexOf('=');
|
|
if (eqIndex === -1) {
|
|
// Could be array continuation or error
|
|
if (!line.startsWith('"') && !line.startsWith("'") && !line.startsWith(']')) {
|
|
result.errors.push({
|
|
line: i + 1,
|
|
message: `Invalid line: ${line} (expected key = value)`,
|
|
});
|
|
result.valid = false;
|
|
}
|
|
continue;
|
|
}
|
|
const key = line.substring(0, eqIndex).trim();
|
|
const valueStr = line.substring(eqIndex + 1).trim();
|
|
// Validate key format
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
result.errors.push({
|
|
line: i + 1,
|
|
message: `Invalid key format: ${key}`,
|
|
});
|
|
result.valid = false;
|
|
continue;
|
|
}
|
|
// Parse value
|
|
let value = valueStr;
|
|
if (valueStr.startsWith('"') && valueStr.endsWith('"')) {
|
|
value = valueStr.slice(1, -1);
|
|
}
|
|
else if (valueStr.startsWith("'") && valueStr.endsWith("'")) {
|
|
value = valueStr.slice(1, -1);
|
|
}
|
|
else if (valueStr === 'true') {
|
|
value = true;
|
|
}
|
|
else if (valueStr === 'false') {
|
|
value = false;
|
|
}
|
|
else if (/^-?\d+$/.test(valueStr)) {
|
|
value = parseInt(valueStr, 10);
|
|
}
|
|
else if (/^-?\d+\.\d+$/.test(valueStr)) {
|
|
value = parseFloat(valueStr);
|
|
}
|
|
else if (valueStr.startsWith('[')) {
|
|
// Array - simplified handling
|
|
value = valueStr;
|
|
}
|
|
// Store in nested structure
|
|
if (currentSection) {
|
|
if (!result.data[currentSection]) {
|
|
result.data[currentSection] = {};
|
|
}
|
|
result.data[currentSection][key] = value;
|
|
}
|
|
else {
|
|
result.data[key] = value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Find the line number for a field
|
|
*/
|
|
function findFieldLine(lines, field) {
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].includes(`${field} =`) || lines[i].includes(`${field}=`)) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
/**
|
|
* Check for common issues in content
|
|
*/
|
|
function checkCommonIssues(content, lines, errors, warnings) {
|
|
// Check for broken links
|
|
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
let match;
|
|
while ((match = linkPattern.exec(content)) !== null) {
|
|
const url = match[2];
|
|
if (url.startsWith('http') && !url.startsWith('https://')) {
|
|
const line = findLineNumber(content, match.index);
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: `Non-HTTPS URL found: ${url}`,
|
|
suggestion: 'Use HTTPS URLs for security',
|
|
});
|
|
}
|
|
}
|
|
// Check for TODO/FIXME comments
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (/\b(TODO|FIXME|XXX|HACK)\b/i.test(line)) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: `Incomplete item found: ${line.trim().substring(0, 50)}...`,
|
|
suggestion: 'Complete or remove TODO/FIXME items before deployment',
|
|
});
|
|
}
|
|
}
|
|
// Check for placeholder content
|
|
const placeholderPatterns = [
|
|
/\[your[- ].*\]/i,
|
|
/\[insert[- ].*\]/i,
|
|
/\[add[- ].*\]/i,
|
|
/\{your[- ].*\}/i,
|
|
/<your[- ].*>/i,
|
|
];
|
|
for (const pattern of placeholderPatterns) {
|
|
if (pattern.test(content)) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: 'Placeholder content detected',
|
|
suggestion: 'Replace placeholder text with actual content',
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Validate markdown structure
|
|
*/
|
|
function validateMarkdownStructure(content, lines, errors, warnings) {
|
|
// Check heading hierarchy
|
|
const headings = extractSections(content);
|
|
let prevLevel = 0;
|
|
for (const heading of headings) {
|
|
if (heading.level > prevLevel + 1 && prevLevel > 0) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: `Heading level jumps from H${prevLevel} to H${heading.level}`,
|
|
suggestion: `Use H${prevLevel + 1} instead of H${heading.level} for proper hierarchy`,
|
|
});
|
|
}
|
|
prevLevel = heading.level;
|
|
}
|
|
// Check for unclosed code blocks
|
|
// Count all triple backticks - they should come in pairs
|
|
const tripleBackticks = (content.match(/```/g) || []).length;
|
|
if (tripleBackticks % 2 !== 0) {
|
|
errors.push({
|
|
path: 'AGENTS.md',
|
|
message: 'Unclosed code block detected (odd number of ``` markers)',
|
|
line: 1,
|
|
});
|
|
}
|
|
// Check for very long lines
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].length > 500) {
|
|
warnings.push({
|
|
path: 'AGENTS.md',
|
|
message: `Very long line (${lines[i].length} chars) at line ${i + 1}`,
|
|
suggestion: 'Consider breaking into multiple lines for readability',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Find line number for a character index
|
|
*/
|
|
function findLineNumber(content, index) {
|
|
return content.substring(0, index).split('\n').length;
|
|
}
|
|
/**
|
|
* Validate MCP server configurations
|
|
*/
|
|
function validateMcpServers(content, lines, errors, warnings) {
|
|
// Find all MCP server sections
|
|
const serverRegex = /\[mcp_servers\.([^\]]+)\]/g;
|
|
const servers = [];
|
|
let match;
|
|
while ((match = serverRegex.exec(content)) !== null) {
|
|
servers.push(match[1]);
|
|
}
|
|
for (const serverName of servers) {
|
|
// Check if server has command
|
|
const serverSection = content.match(new RegExp(`\\[mcp_servers\\.${serverName.replace('.', '\\.')}\\][\\s\\S]*?(?=\\[|$)`));
|
|
if (serverSection) {
|
|
const section = serverSection[0];
|
|
if (!section.includes('command =')) {
|
|
errors.push({
|
|
path: 'config.toml',
|
|
message: `MCP server "${serverName}" missing required "command" field`,
|
|
line: findFieldLine(lines, `[mcp_servers.${serverName}]`),
|
|
});
|
|
}
|
|
// Check for enabled = false (info)
|
|
if (section.includes('enabled = false')) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: `MCP server "${serverName}" is disabled`,
|
|
suggestion: 'Set enabled = true to activate this server',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Validate profiles
|
|
*/
|
|
function validateProfiles(content, lines, errors, warnings) {
|
|
const profileRegex = /\[profiles\.([^\]]+)\]/g;
|
|
const profiles = [];
|
|
let match;
|
|
while ((match = profileRegex.exec(content)) !== null) {
|
|
profiles.push(match[1]);
|
|
}
|
|
// Suggest common profiles if missing
|
|
const recommendedProfiles = ['dev', 'safe', 'ci'];
|
|
for (const profile of recommendedProfiles) {
|
|
if (!profiles.includes(profile)) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: `Consider adding "${profile}" profile`,
|
|
suggestion: `Add [profiles.${profile}] for ${profile === 'dev' ? 'development' : profile === 'safe' ? 'restricted' : 'CI/CD'} environment`,
|
|
});
|
|
}
|
|
}
|
|
// Check profile settings
|
|
for (const profile of profiles) {
|
|
const profileSection = content.match(new RegExp(`\\[profiles\\.${profile}\\][\\s\\S]*?(?=\\[profiles|$)`));
|
|
if (profileSection) {
|
|
const section = profileSection[0];
|
|
// Check if profile has any settings
|
|
if (!section.includes('=')) {
|
|
warnings.push({
|
|
path: 'config.toml',
|
|
message: `Profile "${profile}" has no settings`,
|
|
suggestion: 'Add approval_policy, sandbox_mode, or web_search settings',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=index.js.map
|