llms-py 2.0.20__py3-none-any.whl → 3.0.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llms/__init__.py +3 -1
- llms/db.py +359 -0
- llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +588 -0
- llms/extensions/app/db.py +540 -0
- llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
- llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
- llms/extensions/app/ui/threadStore.mjs +440 -0
- llms/extensions/computer/README.md +96 -0
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/computer/base.py +80 -0
- llms/extensions/computer/bash.py +185 -0
- llms/extensions/computer/computer.py +523 -0
- llms/extensions/computer/edit.py +299 -0
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/computer/platform.py +461 -0
- llms/extensions/computer/run.py +37 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +599 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +260 -0
- llms/extensions/providers/cerebras.py +36 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +559 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/skills/LICENSE +202 -0
- llms/extensions/skills/__init__.py +130 -0
- llms/extensions/skills/errors.py +25 -0
- llms/extensions/skills/models.py +39 -0
- llms/extensions/skills/parser.py +178 -0
- llms/extensions/skills/ui/index.mjs +376 -0
- llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
- llms/extensions/skills/validator.py +177 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +276 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +67 -0
- llms/extensions/tools/ui/index.mjs +837 -0
- llms/index.html +36 -62
- llms/llms.json +180 -879
- llms/main.py +4009 -912
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +176 -8
- llms/ui/ai.mjs +156 -20
- llms/ui/app.css +3768 -321
- llms/ui/ctx.mjs +459 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/highlight.min.mjs +1243 -0
- llms/ui/lib/idb.min.mjs +8 -0
- llms/ui/lib/marked.min.mjs +8 -0
- llms/ui/lib/servicestack-client.mjs +1 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue-router.min.mjs +6 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- llms/ui/markdown.mjs +25 -14
- llms/ui/modules/chat/ChatBody.mjs +1156 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
- llms/ui/modules/chat/index.mjs +995 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +560 -78
- llms/ui/typography.css +54 -36
- llms/ui/utils.mjs +221 -92
- llms_py-3.0.18.dist-info/METADATA +49 -0
- llms_py-3.0.18.dist-info/RECORD +194 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
- llms/ui/Avatar.mjs +0 -28
- llms/ui/Brand.mjs +0 -34
- llms/ui/ChatPrompt.mjs +0 -443
- llms/ui/Main.mjs +0 -740
- llms/ui/ModelSelector.mjs +0 -60
- llms/ui/ProviderIcon.mjs +0 -29
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -36
- llms/ui/Welcome.mjs +0 -8
- llms/ui/threadStore.mjs +0 -524
- llms/ui.json +0 -1069
- llms_py-2.0.20.dist-info/METADATA +0 -931
- llms_py-2.0.20.dist-info/RECORD +0 -36
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
8
|
+
from .parser import read_properties
|
|
9
|
+
|
|
10
|
+
g_skills = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sanitize(name: str) -> str:
|
|
14
|
+
return name.replace(" ", "").replace("_", "").replace("-", "").lower()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def skill(name: Annotated[str, "skill name"], file: Annotated[str | None, "skill file"] = None):
|
|
18
|
+
"""Get the content of a skill or a specific file within a skill."""
|
|
19
|
+
skill = g_skills.get(name)
|
|
20
|
+
|
|
21
|
+
if not skill:
|
|
22
|
+
sanitized_name = sanitize(name)
|
|
23
|
+
for k, v in g_skills.items():
|
|
24
|
+
if sanitize(k) == sanitized_name:
|
|
25
|
+
skill = v
|
|
26
|
+
break
|
|
27
|
+
|
|
28
|
+
if not skill:
|
|
29
|
+
return f"Error: Skill {name} not found. Available skills: {', '.join(g_skills.keys())}"
|
|
30
|
+
location = skill.get("location")
|
|
31
|
+
if not location or not os.path.exists(location):
|
|
32
|
+
return f"Error: Skill {name} not found at location {location}"
|
|
33
|
+
|
|
34
|
+
if file:
|
|
35
|
+
if file.startswith(location):
|
|
36
|
+
file = file[len(location) + 1 :]
|
|
37
|
+
if not os.path.exists(os.path.join(location, file)):
|
|
38
|
+
return f"Error: File {file} not found in skill {name}. Available files: {', '.join(skill.get('files', []))}"
|
|
39
|
+
with open(os.path.join(location, file)) as f:
|
|
40
|
+
return f.read()
|
|
41
|
+
|
|
42
|
+
with open(os.path.join(location, "SKILL.md")) as f:
|
|
43
|
+
content = f.read()
|
|
44
|
+
|
|
45
|
+
files = skill.get("files")
|
|
46
|
+
if files and len(files) > 1:
|
|
47
|
+
content += "\n\n## Skill Files:\n```\n"
|
|
48
|
+
for file in files:
|
|
49
|
+
content += f"{file}\n"
|
|
50
|
+
content += "```\n"
|
|
51
|
+
return content
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def install(ctx):
|
|
55
|
+
global g_skills
|
|
56
|
+
home_skills = ctx.get_home_path(os.path.join(".agent", "skills"))
|
|
57
|
+
# if not folder exists
|
|
58
|
+
if not os.path.exists(home_skills):
|
|
59
|
+
os.makedirs(ctx.get_home_path(os.path.join(".agent")), exist_ok=True)
|
|
60
|
+
ctx.log(f"Creating initial skills folder: {home_skills}")
|
|
61
|
+
# os.makedirs(home_skills)
|
|
62
|
+
# copy ui/skills to home_skills
|
|
63
|
+
ui_skills = os.path.join(ctx.path, "ui", "skills")
|
|
64
|
+
shutil.copytree(ui_skills, home_skills)
|
|
65
|
+
|
|
66
|
+
skill_roots = {}
|
|
67
|
+
|
|
68
|
+
# add .claude skills first, so they can be overridden by .agent skills
|
|
69
|
+
if os.path.exists("~/.claude/skills"):
|
|
70
|
+
skill_roots["~/.claude/skills"] = "~/.claude/skills"
|
|
71
|
+
|
|
72
|
+
if os.path.exists(os.path.join(".claude", "skills")):
|
|
73
|
+
skill_roots[".claude/skills"] = os.path.join(".claude", "skills")
|
|
74
|
+
|
|
75
|
+
skill_roots["~/.llms/.agents"] = home_skills
|
|
76
|
+
|
|
77
|
+
if os.path.exists(os.path.join(".agent", "skills")):
|
|
78
|
+
skill_roots[".agents"] = os.path.join(".agent", "skills")
|
|
79
|
+
|
|
80
|
+
g_skills = {}
|
|
81
|
+
for group, root in skill_roots.items():
|
|
82
|
+
if not os.path.exists(root):
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
for entry in os.scandir(root):
|
|
86
|
+
if (
|
|
87
|
+
entry.is_dir()
|
|
88
|
+
and os.path.exists(os.path.join(entry.path, "SKILL.md"))
|
|
89
|
+
or os.path.exists(os.path.join(entry.path, "skill.md"))
|
|
90
|
+
):
|
|
91
|
+
skill_dir = Path(entry.path).resolve()
|
|
92
|
+
props = read_properties(skill_dir)
|
|
93
|
+
|
|
94
|
+
# recursivly list all files in this directory
|
|
95
|
+
files = []
|
|
96
|
+
for file in skill_dir.glob("**/*"):
|
|
97
|
+
if file.is_file():
|
|
98
|
+
full_path = str(file)
|
|
99
|
+
rel_path = full_path[len(str(skill_dir)) + 1 :]
|
|
100
|
+
files.append(rel_path)
|
|
101
|
+
|
|
102
|
+
skill_props = props.to_dict()
|
|
103
|
+
skill_props.update(
|
|
104
|
+
{
|
|
105
|
+
"group": group,
|
|
106
|
+
"location": str(skill_dir),
|
|
107
|
+
"files": files,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
g_skills[props.name] = skill_props
|
|
111
|
+
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
async def get_skills(request):
|
|
116
|
+
return aiohttp.web.json_response(g_skills)
|
|
117
|
+
|
|
118
|
+
ctx.add_get("", get_skills)
|
|
119
|
+
|
|
120
|
+
async def get_skill(request):
|
|
121
|
+
name = request.match_info.get("name")
|
|
122
|
+
file = request.query.get("file")
|
|
123
|
+
return aiohttp.web.Response(text=skill(name, file))
|
|
124
|
+
|
|
125
|
+
ctx.add_get("contents/{name}", get_skill)
|
|
126
|
+
|
|
127
|
+
ctx.register_tool(skill, group="core_tools")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__install__ = install
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Skill-related exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SkillError(Exception):
|
|
5
|
+
"""Base exception for all skill-related errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParseError(SkillError):
|
|
11
|
+
"""Raised when SKILL.md parsing fails."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidationError(SkillError):
|
|
17
|
+
"""Raised when skill properties are invalid.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
errors: List of validation error messages (may contain just one)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str, errors: list[str] | None = None):
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.errors = errors if errors is not None else [message]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Data models for Agent Skills."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SkillProperties:
|
|
9
|
+
"""Properties parsed from a skill's SKILL.md frontmatter.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
name: Skill name in kebab-case (required)
|
|
13
|
+
description: What the skill does and when the model should use it (required)
|
|
14
|
+
license: License for the skill (optional)
|
|
15
|
+
compatibility: Compatibility information for the skill (optional)
|
|
16
|
+
allowed_tools: Tool patterns the skill requires (optional, experimental)
|
|
17
|
+
metadata: Key-value pairs for client-specific properties (defaults to
|
|
18
|
+
empty dict; omitted from to_dict() output when empty)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
description: str
|
|
23
|
+
license: Optional[str] = None
|
|
24
|
+
compatibility: Optional[str] = None
|
|
25
|
+
allowed_tools: Optional[str] = None
|
|
26
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict:
|
|
29
|
+
"""Convert to dictionary, excluding None values."""
|
|
30
|
+
result = {"name": self.name, "description": self.description}
|
|
31
|
+
if self.license is not None:
|
|
32
|
+
result["license"] = self.license
|
|
33
|
+
if self.compatibility is not None:
|
|
34
|
+
result["compatibility"] = self.compatibility
|
|
35
|
+
if self.allowed_tools is not None:
|
|
36
|
+
result["allowed-tools"] = self.allowed_tools
|
|
37
|
+
if self.metadata:
|
|
38
|
+
result["metadata"] = self.metadata
|
|
39
|
+
return result
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""YAML frontmatter parsing for SKILL.md files."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from .errors import ParseError, ValidationError
|
|
7
|
+
from .models import SkillProperties
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_yaml(content: str) -> dict:
|
|
11
|
+
"""Simple YAML parser for skill frontmatter.
|
|
12
|
+
|
|
13
|
+
Supports:
|
|
14
|
+
- Key-value pairs: key: "value"
|
|
15
|
+
- Comments: # comment
|
|
16
|
+
- Simple nesting (indentation-based)
|
|
17
|
+
"""
|
|
18
|
+
result = {}
|
|
19
|
+
stack = [result]
|
|
20
|
+
indents = [-1]
|
|
21
|
+
last_key = None
|
|
22
|
+
|
|
23
|
+
for line in content.splitlines():
|
|
24
|
+
# Skip empty lines or full comments
|
|
25
|
+
stripped = line.strip()
|
|
26
|
+
if not stripped or stripped.startswith("#"):
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
indent = len(line) - len(line.lstrip())
|
|
30
|
+
|
|
31
|
+
# Handle indent levels
|
|
32
|
+
while indent <= indents[-1]:
|
|
33
|
+
indents.pop()
|
|
34
|
+
stack.pop()
|
|
35
|
+
|
|
36
|
+
# If we have a nested block under last key
|
|
37
|
+
if indent > indents[-1] and last_key and isinstance(stack[-1], dict) and stack[-1].get(last_key) is None:
|
|
38
|
+
# This branch is tricky with the simple look-behind.
|
|
39
|
+
# Better approach: check if line is a key-value or array item
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
# Parse key: value
|
|
43
|
+
if ":" in stripped:
|
|
44
|
+
key, val = stripped.split(":", 1)
|
|
45
|
+
key = key.strip()
|
|
46
|
+
val = val.strip()
|
|
47
|
+
|
|
48
|
+
# Handle quotes
|
|
49
|
+
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
|
|
50
|
+
val = val[1:-1]
|
|
51
|
+
elif val.lower() == "true":
|
|
52
|
+
val = True
|
|
53
|
+
elif val.lower() == "false":
|
|
54
|
+
val = False
|
|
55
|
+
elif val == "":
|
|
56
|
+
val = None # Could be start of nested object
|
|
57
|
+
|
|
58
|
+
current_dict = stack[-1]
|
|
59
|
+
|
|
60
|
+
if val is None:
|
|
61
|
+
# Prepare for nested object
|
|
62
|
+
new_dict = {}
|
|
63
|
+
current_dict[key] = new_dict
|
|
64
|
+
stack.append(new_dict)
|
|
65
|
+
indents.append(indent)
|
|
66
|
+
else:
|
|
67
|
+
current_dict[key] = val
|
|
68
|
+
|
|
69
|
+
last_key = key
|
|
70
|
+
else:
|
|
71
|
+
# Handle continuation lines or unknown format if needed,
|
|
72
|
+
# but for our simple use case we might error or ignore.
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_skill_md(skill_dir: Path) -> Optional[Path]:
|
|
79
|
+
"""Find the SKILL.md file in a skill directory.
|
|
80
|
+
|
|
81
|
+
Prefers SKILL.md (uppercase) but accepts skill.md (lowercase).
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
skill_dir: Path to the skill directory
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Path to the SKILL.md file, or None if not found
|
|
88
|
+
"""
|
|
89
|
+
for name in ("SKILL.md", "skill.md"):
|
|
90
|
+
path = skill_dir / name
|
|
91
|
+
if path.exists():
|
|
92
|
+
return path
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_frontmatter(content: str) -> tuple[dict, str]:
|
|
97
|
+
"""Parse YAML frontmatter from SKILL.md content.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
content: Raw content of SKILL.md file
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tuple of (metadata dict, markdown body)
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ParseError: If frontmatter is missing or invalid
|
|
107
|
+
"""
|
|
108
|
+
if not content.startswith("---"):
|
|
109
|
+
raise ParseError("SKILL.md must start with YAML frontmatter (---)")
|
|
110
|
+
|
|
111
|
+
parts = content.split("---", 2)
|
|
112
|
+
if len(parts) < 3:
|
|
113
|
+
raise ParseError("SKILL.md frontmatter not properly closed with ---")
|
|
114
|
+
|
|
115
|
+
frontmatter_str = parts[1]
|
|
116
|
+
body = parts[2].strip()
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
metadata = load_yaml(frontmatter_str)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise ParseError(f"Invalid YAML in frontmatter: {e}") from e
|
|
122
|
+
|
|
123
|
+
if not isinstance(metadata, dict):
|
|
124
|
+
raise ParseError("SKILL.md frontmatter must be a YAML mapping")
|
|
125
|
+
|
|
126
|
+
# Clean up metadata values if necessary (simple parser already handles basics)
|
|
127
|
+
if "metadata" in metadata and isinstance(metadata["metadata"], dict):
|
|
128
|
+
metadata["metadata"] = {str(k): str(v) for k, v in metadata["metadata"].items()}
|
|
129
|
+
|
|
130
|
+
return metadata, body
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_properties(skill_dir: Path) -> SkillProperties:
|
|
134
|
+
"""Read skill properties from SKILL.md frontmatter.
|
|
135
|
+
|
|
136
|
+
This function parses the frontmatter and returns properties.
|
|
137
|
+
It does NOT perform full validation. Use validate() for that.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
skill_dir: Path to the skill directory
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
SkillProperties with parsed metadata
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ParseError: If SKILL.md is missing or has invalid YAML
|
|
147
|
+
ValidationError: If required fields (name, description) are missing
|
|
148
|
+
"""
|
|
149
|
+
skill_dir = Path(skill_dir)
|
|
150
|
+
skill_md = find_skill_md(skill_dir)
|
|
151
|
+
|
|
152
|
+
if skill_md is None:
|
|
153
|
+
raise ParseError(f"SKILL.md not found in {skill_dir}")
|
|
154
|
+
|
|
155
|
+
content = skill_md.read_text()
|
|
156
|
+
metadata, _ = parse_frontmatter(content)
|
|
157
|
+
|
|
158
|
+
if "name" not in metadata:
|
|
159
|
+
raise ValidationError("Missing required field in frontmatter: name")
|
|
160
|
+
if "description" not in metadata:
|
|
161
|
+
raise ValidationError("Missing required field in frontmatter: description")
|
|
162
|
+
|
|
163
|
+
name = metadata["name"]
|
|
164
|
+
description = metadata["description"]
|
|
165
|
+
|
|
166
|
+
if not isinstance(name, str) or not name.strip():
|
|
167
|
+
raise ValidationError("Field 'name' must be a non-empty string")
|
|
168
|
+
if not isinstance(description, str) or not description.strip():
|
|
169
|
+
raise ValidationError("Field 'description' must be a non-empty string")
|
|
170
|
+
|
|
171
|
+
return SkillProperties(
|
|
172
|
+
name=name.strip(),
|
|
173
|
+
description=description.strip(),
|
|
174
|
+
license=metadata.get("license"),
|
|
175
|
+
compatibility=metadata.get("compatibility"),
|
|
176
|
+
allowed_tools=metadata.get("allowed-tools"),
|
|
177
|
+
metadata=metadata.get("metadata"),
|
|
178
|
+
)
|