Harden SKILL.md parser — error logging, flexible indent, CRLF support, type validation
This commit is contained in:
@@ -67,13 +67,22 @@ except FileNotFoundError:
|
|||||||
|
|
||||||
|
|
||||||
def parse_skill_md(path):
|
def parse_skill_md(path):
|
||||||
"""Parse a SKILL.md frontmatter into a tool definition."""
|
"""Parse a SKILL.md frontmatter into a tool definition.
|
||||||
with open(path) as f:
|
Returns tool definition dict or None on failure."""
|
||||||
content = f.read()
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
content = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Cannot read {path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Normalize line endings
|
||||||
|
content = content.replace("\r\n", "\n")
|
||||||
|
|
||||||
# Extract YAML frontmatter between ---
|
# Extract YAML frontmatter between ---
|
||||||
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||||
if not match:
|
if not match:
|
||||||
|
log(f"No frontmatter in {path}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Simple YAML-like parser (no pyyaml dependency)
|
# Simple YAML-like parser (no pyyaml dependency)
|
||||||
@@ -87,21 +96,24 @@ def parse_skill_md(path):
|
|||||||
if not stripped or stripped.startswith("#"):
|
if not stripped or stripped.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startswith(" ") and current_key == "parameters":
|
# Detect indent level (flexible — 2+ spaces counts as nested)
|
||||||
# Inside parameters block
|
indent = len(line) - len(line.lstrip())
|
||||||
if line.startswith(" ") and current_param:
|
|
||||||
|
if indent >= 2 and current_key == "parameters":
|
||||||
|
if indent >= 4 and current_param:
|
||||||
# Parameter property
|
# Parameter property
|
||||||
k, _, v = stripped.partition(":")
|
k, _, v = stripped.partition(":")
|
||||||
|
k = k.strip()
|
||||||
v = v.strip().strip('"').strip("'")
|
v = v.strip().strip('"').strip("'")
|
||||||
if k.strip() == "required":
|
if k == "required":
|
||||||
v = v.lower() == "true"
|
v = v.lower() in ("true", "yes", "1")
|
||||||
params[current_param][k.strip()] = v
|
params[current_param][k] = v
|
||||||
elif ":" in stripped:
|
elif ":" in stripped:
|
||||||
# New parameter
|
# New parameter name
|
||||||
param_name = stripped.rstrip(":").strip()
|
param_name = stripped.rstrip(":").strip()
|
||||||
current_param = param_name
|
current_param = param_name
|
||||||
params[param_name] = {}
|
params[param_name] = {}
|
||||||
elif ":" in line and not line.startswith(" "):
|
elif ":" in line and indent == 0:
|
||||||
k, _, v = line.partition(":")
|
k, _, v = line.partition(":")
|
||||||
k = k.strip()
|
k = k.strip()
|
||||||
v = v.strip().strip('"').strip("'")
|
v = v.strip().strip('"').strip("'")
|
||||||
@@ -111,14 +123,21 @@ def parse_skill_md(path):
|
|||||||
current_param = None
|
current_param = None
|
||||||
|
|
||||||
if "name" not in fm:
|
if "name" not in fm:
|
||||||
|
log(f"No 'name' field in {path}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if "description" not in fm:
|
||||||
|
log(f"Warning: no 'description' in {path}")
|
||||||
|
|
||||||
# Build Ollama tool definition
|
# Build Ollama tool definition
|
||||||
properties = {}
|
properties = {}
|
||||||
required = []
|
required = []
|
||||||
for pname, pdata in params.items():
|
for pname, pdata in params.items():
|
||||||
|
ptype = pdata.get("type", "string")
|
||||||
|
if ptype not in ("string", "integer", "number", "boolean", "array", "object"):
|
||||||
|
log(f"Warning: unknown type '{ptype}' for param '{pname}' in {path}")
|
||||||
properties[pname] = {
|
properties[pname] = {
|
||||||
"type": pdata.get("type", "string"),
|
"type": ptype,
|
||||||
"description": pdata.get("description", ""),
|
"description": pdata.get("description", ""),
|
||||||
}
|
}
|
||||||
if pdata.get("required", False):
|
if pdata.get("required", False):
|
||||||
|
|||||||
Reference in New Issue
Block a user