YAML Frontmatter Parsing & Debug Parsing
- Add skill_parsing.hpp and skill_parsing.cpp for YAML frontmatter parsing - Extract name (required) and description (optional) from SKILL.md files - Add -y flag to print parsed metadata and exit - Add test_skill_parsing.sh to verify the -y flag functionality - Update CMakeLists.txt to include the new source and test
This commit is contained in:
@@ -30,6 +30,7 @@ add_library(drharness_lib
|
||||
src/debug_client.cpp
|
||||
src/editor.cpp
|
||||
src/skill_scanner.cpp
|
||||
src/skill_parsing.cpp
|
||||
${TOOL_SOURCES}
|
||||
)
|
||||
target_include_directories(drharness_lib PUBLIC include tools)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
// Represents parsed YAML frontmatter metadata from a SKILL.md file
|
||||
struct SkillMetadata {
|
||||
std::string name;
|
||||
std::optional<std::string> description;
|
||||
};
|
||||
|
||||
// Parse YAML frontmatter from a file.
|
||||
// Throws std::runtime_error if the file is malformed or name is missing.
|
||||
SkillMetadata parse_skill_frontmatter(const std::string& filepath);
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "file_read_tool.hpp"
|
||||
#include "grep_tool.hpp"
|
||||
#include "skill_scanner.hpp"
|
||||
#include "skill_parsing.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
@@ -47,6 +48,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
bool use_debug = false;
|
||||
bool debug_paths = false;
|
||||
char const* debug_frontmatter = nullptr;
|
||||
|
||||
ToolRegistry master_registery;
|
||||
ToolRegistry agent_tools;
|
||||
@@ -127,6 +129,11 @@ int main(int argc, char** argv) {
|
||||
consume_arg();
|
||||
debug_paths = true;
|
||||
}
|
||||
else if (argv[0] == std::string("-y"))
|
||||
{
|
||||
consume_arg();
|
||||
debug_frontmatter = consume_arg();
|
||||
}
|
||||
else if (argv[0] == std::string("-L"))
|
||||
{
|
||||
consume_arg();
|
||||
@@ -159,6 +166,7 @@ int main(int argc, char** argv) {
|
||||
<< " -l List available tools\n"
|
||||
<< " -L List available models on server side\n"
|
||||
<< " -D Debug: print all discovered skill paths and exit\n"
|
||||
<< " -y <file> Debug: print YAML frontmatter from a SKILL.md file and exit\n"
|
||||
<< " -d Use debug client (mock API via stdin)\n"
|
||||
<< " -h Display this help menu\n";
|
||||
exit(0);
|
||||
@@ -185,6 +193,20 @@ int main(int argc, char** argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (debug_frontmatter) {
|
||||
try {
|
||||
auto metadata = parse_skill_frontmatter(debug_frontmatter);
|
||||
std::cout << "name: " << metadata.name << "\n";
|
||||
if (metadata.description.has_value()) {
|
||||
std::cout << "description: " << metadata.description.value() << "\n";
|
||||
}
|
||||
} catch (const std::runtime_error& e) {
|
||||
std::cerr << "Error parsing frontmatter: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (list_models)
|
||||
{
|
||||
auto models = create_client(use_debug)->list_models();
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "skill_parsing.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
SkillMetadata parse_skill_frontmatter(const std::string& filepath) {
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Cannot open file: " + filepath);
|
||||
}
|
||||
|
||||
std::string line;
|
||||
bool in_frontmatter = false;
|
||||
std::string name;
|
||||
std::optional<std::string> description;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
// Remove trailing carriage return if present
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
if (!in_frontmatter) {
|
||||
// Look for opening ---
|
||||
if (line == "---") {
|
||||
in_frontmatter = true;
|
||||
continue;
|
||||
}
|
||||
// If we hit a non-empty line before ---, it's not valid frontmatter
|
||||
if (!line.empty()) {
|
||||
throw std::runtime_error("File does not start with YAML frontmatter (---)");
|
||||
}
|
||||
} else {
|
||||
// Look for closing ---
|
||||
if (line == "---") {
|
||||
if (name.empty()) {
|
||||
throw std::runtime_error("YAML frontmatter is missing required 'name' field");
|
||||
}
|
||||
// Successfully parsed frontmatter
|
||||
SkillMetadata metadata;
|
||||
metadata.name = name;
|
||||
metadata.description = description;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// Parse YAML key-value pair (simple key: value format)
|
||||
if (line.find(':') != std::string::npos) {
|
||||
size_t colon_pos = line.find(':');
|
||||
std::string key = line.substr(0, colon_pos);
|
||||
std::string value = line.substr(colon_pos + 1);
|
||||
|
||||
// Trim leading space from value
|
||||
if (!value.empty() && value[0] == ' ') {
|
||||
value = value.substr(1);
|
||||
}
|
||||
|
||||
// Remove surrounding quotes from value if present
|
||||
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
|
||||
value = value.substr(1, value.size() - 2);
|
||||
} else if (value.size() >= 2 && value.front() == '\'' && value.back() == '\'') {
|
||||
value = value.substr(1, value.size() - 2);
|
||||
}
|
||||
|
||||
if (key == "name") {
|
||||
if (name.empty()) {
|
||||
name = value;
|
||||
}
|
||||
} else if (key == "description") {
|
||||
description = value;
|
||||
}
|
||||
// Ignore unknown keys
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error("YAML frontmatter is not properly closed (missing closing ---)");
|
||||
}
|
||||
@@ -27,6 +27,7 @@ add_dr_test(test_load_chat)
|
||||
add_dr_test(process_reasoning)
|
||||
add_dr_test(test_skill_scanner)
|
||||
add_dr_test(test_nested_skills)
|
||||
add_dr_test(test_skill_parsing)
|
||||
|
||||
# Add editor tests
|
||||
add_dr_test(test_editor)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# test_skill_parsing.sh - Verifies the -y flag correctly parses YAML frontmatter
|
||||
|
||||
# Create test skill files
|
||||
echo '# Test Skill' > "$TEST_TEMP_DIR/test_skill.md"
|
||||
|
||||
# Test 1: Skill with name and description
|
||||
cat > "$TEST_TEMP_DIR/full_skill.md" << 'SKILL'
|
||||
---
|
||||
name: full-skill
|
||||
description: A full skill with description
|
||||
---
|
||||
|
||||
# Full Skill Content
|
||||
|
||||
This is the full skill.
|
||||
SKILL
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -y "$TEST_TEMP_DIR/full_skill.md" -F dummy.chat)
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "name: full-skill"; then
|
||||
echo "FAIL: Expected 'name: full-skill' in output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "description: A full skill with description"; then
|
||||
echo "FAIL: Expected description in output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Skill with only name (no description)
|
||||
cat > "$TEST_TEMP_DIR/name_only_skill.md" << 'SKILL'
|
||||
---
|
||||
name: name-only-skill
|
||||
---
|
||||
|
||||
# Name Only Skill Content
|
||||
SKILL
|
||||
|
||||
OUTPUT=$("${DRHARNESS}" -y "$TEST_TEMP_DIR/name_only_skill.md" -F dummy.chat)
|
||||
|
||||
if ! echo "$OUTPUT" | grep -q "name: name-only-skill"; then
|
||||
echo "FAIL: Expected 'name: name-only-skill' in output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "description:"; then
|
||||
echo "FAIL: Unexpected description in output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Missing name should fail
|
||||
cat > "$TEST_TEMP_DIR/missing_name_skill.md" << 'SKILL'
|
||||
---
|
||||
description: No name here
|
||||
---
|
||||
|
||||
# Missing Name Skill
|
||||
SKILL
|
||||
|
||||
set +e
|
||||
"${DRHARNESS}" -y "$TEST_TEMP_DIR/missing_name_skill.md" -F dummy.chat 2>/dev/null
|
||||
MISSING_NAME_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ $MISSING_NAME_EXIT -eq 0 ]; then
|
||||
echo "FAIL: Expected failure when name is missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Invalid frontmatter (no closing ---) should fail
|
||||
cat > "$TEST_TEMP_DIR/invalid_skill.md" << 'SKILL'
|
||||
---
|
||||
name: invalid-skill
|
||||
description: No closing
|
||||
|
||||
# Invalid Skill
|
||||
SKILL
|
||||
|
||||
set +e
|
||||
"${DRHARNESS}" -y "$TEST_TEMP_DIR/invalid_skill.md" -F dummy.chat 2>/dev/null
|
||||
INVALID_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ $INVALID_EXIT -eq 0 ]; then
|
||||
echo "FAIL: Expected failure for invalid frontmatter"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 5: File without frontmatter should fail
|
||||
cat > "$TEST_TEMP_DIR/no_frontmatter_skill.md" << 'SKILL'
|
||||
# No Frontmatter Skill
|
||||
|
||||
This skill has no frontmatter.
|
||||
SKILL
|
||||
|
||||
set +e
|
||||
"${DRHARNESS}" -y "$TEST_TEMP_DIR/no_frontmatter_skill.md" -F dummy.chat 2>/dev/null
|
||||
NO_FRONTMATTER_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ $NO_FRONTMATTER_EXIT -eq 0 ]; then
|
||||
echo "FAIL: Expected failure for file without frontmatter"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All tests passed!"
|
||||
Reference in New Issue
Block a user