abstractskill 0.1.0__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.
- abstractskill/__init__.py +33 -0
- abstractskill/errors.py +17 -0
- abstractskill/hash.py +14 -0
- abstractskill/loader.py +75 -0
- abstractskill/models.py +59 -0
- abstractskill/parser.py +117 -0
- abstractskill/prompt.py +22 -0
- abstractskill/validation.py +34 -0
- abstractskill-0.1.0.dist-info/METADATA +96 -0
- abstractskill-0.1.0.dist-info/RECORD +13 -0
- abstractskill-0.1.0.dist-info/WHEEL +5 -0
- abstractskill-0.1.0.dist-info/licenses/LICENSE +21 -0
- abstractskill-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""AbstractSkill — Agent Skills parsing and discovery for AbstractFramework."""
|
|
2
|
+
|
|
3
|
+
from abstractskill.errors import (
|
|
4
|
+
SkillError,
|
|
5
|
+
SkillNotFoundError,
|
|
6
|
+
SkillParseError,
|
|
7
|
+
SkillValidationError,
|
|
8
|
+
)
|
|
9
|
+
from abstractskill.hash import content_hash
|
|
10
|
+
from abstractskill.loader import FilesystemSkillLoader
|
|
11
|
+
from abstractskill.models import LoadedSkill, SkillDocument, SkillMetadata
|
|
12
|
+
from abstractskill.parser import parse_skill_md
|
|
13
|
+
from abstractskill.prompt import format_available_skills_xml
|
|
14
|
+
from abstractskill.validation import SKILL_FILENAME, SKILL_NAME_RE, validate_skill_name
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"FilesystemSkillLoader",
|
|
18
|
+
"LoadedSkill",
|
|
19
|
+
"SKILL_FILENAME",
|
|
20
|
+
"SKILL_NAME_RE",
|
|
21
|
+
"SkillDocument",
|
|
22
|
+
"SkillError",
|
|
23
|
+
"SkillMetadata",
|
|
24
|
+
"SkillNotFoundError",
|
|
25
|
+
"SkillParseError",
|
|
26
|
+
"SkillValidationError",
|
|
27
|
+
"content_hash",
|
|
28
|
+
"format_available_skills_xml",
|
|
29
|
+
"parse_skill_md",
|
|
30
|
+
"validate_skill_name",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
abstractskill/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Skill-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SkillError(Exception):
|
|
5
|
+
"""Base error for AbstractSkill operations."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SkillParseError(SkillError):
|
|
9
|
+
"""Raised when SKILL.md content cannot be parsed."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SkillValidationError(SkillError):
|
|
13
|
+
"""Raised when a skill document or directory layout is invalid."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SkillNotFoundError(SkillError):
|
|
17
|
+
"""Raised when a requested skill cannot be resolved."""
|
abstractskill/hash.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Content hashing for skill evolution tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def content_hash(content: str | bytes) -> str:
|
|
9
|
+
"""Return a stable SHA-256 hex digest for skill snapshotting."""
|
|
10
|
+
if isinstance(content, str):
|
|
11
|
+
payload = content.encode("utf-8")
|
|
12
|
+
else:
|
|
13
|
+
payload = content
|
|
14
|
+
return hashlib.sha256(payload).hexdigest()
|
abstractskill/loader.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Filesystem discovery and loading for Agent Skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from abstractskill.errors import SkillNotFoundError, SkillParseError, SkillValidationError
|
|
8
|
+
from abstractskill.models import LoadedSkill, SkillMetadata
|
|
9
|
+
from abstractskill.parser import parse_skill_md
|
|
10
|
+
from abstractskill.validation import SKILL_FILENAME, validate_directory_name_match
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FilesystemSkillLoader:
|
|
14
|
+
"""Discover and load skills from one or more directory roots.
|
|
15
|
+
|
|
16
|
+
Later roots override earlier ones when skill names collide.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, roots: Path | str | list[Path | str]) -> None:
|
|
20
|
+
if isinstance(roots, (str, Path)):
|
|
21
|
+
root_list = [roots]
|
|
22
|
+
else:
|
|
23
|
+
root_list = list(roots)
|
|
24
|
+
self._roots = [Path(root).expanduser() for root in root_list]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def roots(self) -> tuple[Path, ...]:
|
|
28
|
+
return tuple(self._roots)
|
|
29
|
+
|
|
30
|
+
def discover(self) -> list[SkillMetadata]:
|
|
31
|
+
"""Return metadata for all valid skills, sorted by name."""
|
|
32
|
+
by_name: dict[str, SkillMetadata] = {}
|
|
33
|
+
for root in self._roots:
|
|
34
|
+
if not root.is_dir():
|
|
35
|
+
continue
|
|
36
|
+
for child in sorted(root.iterdir()):
|
|
37
|
+
if not child.is_dir():
|
|
38
|
+
continue
|
|
39
|
+
skill_file = child / SKILL_FILENAME
|
|
40
|
+
if not skill_file.is_file():
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
document = parse_skill_md(
|
|
44
|
+
skill_file.read_text(encoding="utf-8"),
|
|
45
|
+
source_path=skill_file,
|
|
46
|
+
directory_name=child.name,
|
|
47
|
+
)
|
|
48
|
+
except (SkillParseError, SkillValidationError):
|
|
49
|
+
continue
|
|
50
|
+
by_name[document.metadata.name] = document.metadata
|
|
51
|
+
return [by_name[name] for name in sorted(by_name)]
|
|
52
|
+
|
|
53
|
+
def load(self, name: str) -> LoadedSkill:
|
|
54
|
+
"""Load the full SKILL.md document for a skill by name."""
|
|
55
|
+
for root in reversed(self._roots):
|
|
56
|
+
if not root.is_dir():
|
|
57
|
+
continue
|
|
58
|
+
candidate = root / name
|
|
59
|
+
skill_file = candidate / SKILL_FILENAME
|
|
60
|
+
if not skill_file.is_file():
|
|
61
|
+
continue
|
|
62
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
63
|
+
document = parse_skill_md(
|
|
64
|
+
text,
|
|
65
|
+
source_path=skill_file,
|
|
66
|
+
directory_name=candidate.name,
|
|
67
|
+
)
|
|
68
|
+
if document.metadata.name != name:
|
|
69
|
+
raise SkillValidationError(
|
|
70
|
+
f"skill at {skill_file} declares name {document.metadata.name!r}, "
|
|
71
|
+
f"expected {name!r}"
|
|
72
|
+
)
|
|
73
|
+
validate_directory_name_match(document.metadata.name, candidate.name)
|
|
74
|
+
return LoadedSkill(document=document, root_dir=candidate)
|
|
75
|
+
raise SkillNotFoundError(f"skill not found: {name}")
|
abstractskill/models.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Data models for Agent Skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class SkillMetadata:
|
|
12
|
+
"""Metadata-only skill view used for discovery and progressive disclosure."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
description: str
|
|
16
|
+
license: str | None = None
|
|
17
|
+
compatibility: str | None = None
|
|
18
|
+
allowed_tools: tuple[str, ...] = ()
|
|
19
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
20
|
+
source_path: Path | None = None
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict[str, Any]:
|
|
23
|
+
payload: dict[str, Any] = {
|
|
24
|
+
"name": self.name,
|
|
25
|
+
"description": self.description,
|
|
26
|
+
}
|
|
27
|
+
if self.license is not None:
|
|
28
|
+
payload["license"] = self.license
|
|
29
|
+
if self.compatibility is not None:
|
|
30
|
+
payload["compatibility"] = self.compatibility
|
|
31
|
+
if self.allowed_tools:
|
|
32
|
+
payload["allowed_tools"] = list(self.allowed_tools)
|
|
33
|
+
if self.metadata:
|
|
34
|
+
payload["metadata"] = dict(self.metadata)
|
|
35
|
+
if self.source_path is not None:
|
|
36
|
+
payload["source_path"] = str(self.source_path)
|
|
37
|
+
return payload
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class SkillDocument:
|
|
42
|
+
"""Full SKILL.md content: frontmatter metadata plus markdown instructions."""
|
|
43
|
+
|
|
44
|
+
metadata: SkillMetadata
|
|
45
|
+
body: str
|
|
46
|
+
raw: str
|
|
47
|
+
content_hash: str
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def name(self) -> str:
|
|
51
|
+
return self.metadata.name
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class LoadedSkill:
|
|
56
|
+
"""A skill resolved from disk with its root directory."""
|
|
57
|
+
|
|
58
|
+
document: SkillDocument
|
|
59
|
+
root_dir: Path
|
abstractskill/parser.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""SKILL.md parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from abstractskill.errors import SkillParseError, SkillValidationError
|
|
11
|
+
from abstractskill.hash import content_hash
|
|
12
|
+
from abstractskill.models import SkillDocument, SkillMetadata
|
|
13
|
+
from abstractskill.validation import validate_skill_name
|
|
14
|
+
|
|
15
|
+
_FRONTMATTER_DELIM = "---"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _split_frontmatter(text: str) -> tuple[Mapping[str, Any], str]:
|
|
19
|
+
stripped = text.lstrip("\ufeff")
|
|
20
|
+
if not stripped.startswith(f"{_FRONTMATTER_DELIM}\n"):
|
|
21
|
+
raise SkillParseError("SKILL.md must start with YAML frontmatter")
|
|
22
|
+
|
|
23
|
+
lines = stripped.splitlines()
|
|
24
|
+
if not lines or lines[0].strip() != _FRONTMATTER_DELIM:
|
|
25
|
+
raise SkillParseError("SKILL.md frontmatter opening delimiter is missing")
|
|
26
|
+
|
|
27
|
+
closing_idx = None
|
|
28
|
+
for idx in range(1, len(lines)):
|
|
29
|
+
if lines[idx].strip() == _FRONTMATTER_DELIM:
|
|
30
|
+
closing_idx = idx
|
|
31
|
+
break
|
|
32
|
+
if closing_idx is None:
|
|
33
|
+
raise SkillParseError("SKILL.md frontmatter closing delimiter is missing")
|
|
34
|
+
|
|
35
|
+
frontmatter_text = "\n".join(lines[1:closing_idx])
|
|
36
|
+
body = "\n".join(lines[closing_idx + 1 :])
|
|
37
|
+
if body.startswith("\n"):
|
|
38
|
+
body = body[1:]
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
loaded = yaml.safe_load(frontmatter_text) or {}
|
|
42
|
+
except yaml.YAMLError as exc:
|
|
43
|
+
raise SkillParseError(f"invalid YAML frontmatter: {exc}") from exc
|
|
44
|
+
|
|
45
|
+
if not isinstance(loaded, dict):
|
|
46
|
+
raise SkillParseError("SKILL.md frontmatter must be a mapping")
|
|
47
|
+
return loaded, body
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_allowed_tools(raw: Any) -> tuple[str, ...]:
|
|
51
|
+
if raw is None:
|
|
52
|
+
return ()
|
|
53
|
+
if isinstance(raw, str):
|
|
54
|
+
tokens = raw.split()
|
|
55
|
+
elif isinstance(raw, list):
|
|
56
|
+
tokens = [str(item).strip() for item in raw if str(item).strip()]
|
|
57
|
+
else:
|
|
58
|
+
raise SkillValidationError("allowed-tools must be a string or list of strings")
|
|
59
|
+
return tuple(tokens)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _metadata_from_frontmatter(
|
|
63
|
+
frontmatter: Mapping[str, Any],
|
|
64
|
+
*,
|
|
65
|
+
source_path: Path | None = None,
|
|
66
|
+
) -> SkillMetadata:
|
|
67
|
+
if "name" not in frontmatter:
|
|
68
|
+
raise SkillValidationError("SKILL.md frontmatter requires 'name'")
|
|
69
|
+
if "description" not in frontmatter:
|
|
70
|
+
raise SkillValidationError("SKILL.md frontmatter requires 'description'")
|
|
71
|
+
|
|
72
|
+
name = validate_skill_name(str(frontmatter["name"]))
|
|
73
|
+
description = str(frontmatter["description"]).strip()
|
|
74
|
+
if not description:
|
|
75
|
+
raise SkillValidationError("SKILL.md frontmatter requires a non-empty description")
|
|
76
|
+
|
|
77
|
+
license_value = frontmatter.get("license")
|
|
78
|
+
compatibility = frontmatter.get("compatibility")
|
|
79
|
+
extra = frontmatter.get("metadata")
|
|
80
|
+
if extra is not None and not isinstance(extra, dict):
|
|
81
|
+
raise SkillValidationError("metadata frontmatter field must be a mapping")
|
|
82
|
+
|
|
83
|
+
allowed_raw = frontmatter.get("allowed-tools", frontmatter.get("allowed_tools"))
|
|
84
|
+
allowed_tools = _parse_allowed_tools(allowed_raw)
|
|
85
|
+
|
|
86
|
+
return SkillMetadata(
|
|
87
|
+
name=name,
|
|
88
|
+
description=description,
|
|
89
|
+
license=str(license_value).strip() if license_value is not None else None,
|
|
90
|
+
compatibility=str(compatibility).strip() if compatibility is not None else None,
|
|
91
|
+
allowed_tools=allowed_tools,
|
|
92
|
+
metadata=dict(extra or {}),
|
|
93
|
+
source_path=source_path,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_skill_md(
|
|
98
|
+
text: str,
|
|
99
|
+
*,
|
|
100
|
+
source_path: Path | None = None,
|
|
101
|
+
directory_name: str | None = None,
|
|
102
|
+
) -> SkillDocument:
|
|
103
|
+
"""Parse SKILL.md text into metadata and instructions."""
|
|
104
|
+
frontmatter, body = _split_frontmatter(text)
|
|
105
|
+
metadata = _metadata_from_frontmatter(frontmatter, source_path=source_path)
|
|
106
|
+
|
|
107
|
+
if directory_name is not None:
|
|
108
|
+
from abstractskill.validation import validate_directory_name_match
|
|
109
|
+
|
|
110
|
+
validate_directory_name_match(metadata.name, directory_name)
|
|
111
|
+
|
|
112
|
+
return SkillDocument(
|
|
113
|
+
metadata=metadata,
|
|
114
|
+
body=body,
|
|
115
|
+
raw=text,
|
|
116
|
+
content_hash=content_hash(text),
|
|
117
|
+
)
|
abstractskill/prompt.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Prompt helpers for progressive skill disclosure."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
|
|
7
|
+
from abstractskill.models import SkillMetadata
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_available_skills_xml(skills: list[SkillMetadata]) -> str:
|
|
11
|
+
"""Render a deterministic `<available_skills>` block for host prompts."""
|
|
12
|
+
if not skills:
|
|
13
|
+
return "<available_skills></available_skills>"
|
|
14
|
+
|
|
15
|
+
lines = ["<available_skills>"]
|
|
16
|
+
for skill in sorted(skills, key=lambda item: item.name):
|
|
17
|
+
lines.append(" <skill>")
|
|
18
|
+
lines.append(f" <name>{html.escape(skill.name)}</name>")
|
|
19
|
+
lines.append(f" <description>{html.escape(skill.description)}</description>")
|
|
20
|
+
lines.append(" </skill>")
|
|
21
|
+
lines.append("</available_skills>")
|
|
22
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Validation helpers for Agent Skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from abstractskill.errors import SkillValidationError
|
|
8
|
+
|
|
9
|
+
SKILL_NAME_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$")
|
|
10
|
+
SKILL_FILENAME = "SKILL.md"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_skill_name(name: str) -> str:
|
|
14
|
+
"""Validate an Agent Skills `name` field."""
|
|
15
|
+
if not isinstance(name, str):
|
|
16
|
+
raise SkillValidationError("skill name must be a string")
|
|
17
|
+
normalized = name.strip()
|
|
18
|
+
if not normalized:
|
|
19
|
+
raise SkillValidationError("skill name is required")
|
|
20
|
+
if len(normalized) > 64:
|
|
21
|
+
raise SkillValidationError("skill name must be at most 64 characters")
|
|
22
|
+
if not SKILL_NAME_RE.fullmatch(normalized):
|
|
23
|
+
raise SkillValidationError(
|
|
24
|
+
"skill name must use lowercase letters, digits, and hyphens only"
|
|
25
|
+
)
|
|
26
|
+
return normalized
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_directory_name_match(name: str, directory_name: str) -> None:
|
|
30
|
+
"""Ensure the skill name matches its leaf directory name."""
|
|
31
|
+
if name != directory_name:
|
|
32
|
+
raise SkillValidationError(
|
|
33
|
+
f"skill name {name!r} must match directory name {directory_name!r}"
|
|
34
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstractskill
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Skills (SKILL.md) parsing, discovery, and evolution tracking for AbstractFramework
|
|
5
|
+
Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lpalbou/AbstractSkill
|
|
8
|
+
Project-URL: Repository, https://github.com/lpalbou/AbstractSkill
|
|
9
|
+
Project-URL: Documentation, https://github.com/lpalbou/AbstractSkill#readme
|
|
10
|
+
Keywords: abstractframework,abstractcore,abstractgateway,agent-skills,skill-md,skills
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: PyYAML>=6.0
|
|
24
|
+
Provides-Extra: apple
|
|
25
|
+
Provides-Extra: all-apple
|
|
26
|
+
Provides-Extra: gpu
|
|
27
|
+
Provides-Extra: all-gpu
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# AbstractSkill
|
|
35
|
+
|
|
36
|
+
AbstractSkill is the shared Python library for [Agent Skills](https://agentskills.io/) (`SKILL.md`) in the
|
|
37
|
+
[AbstractFramework](https://github.com/lpalbou/AbstractFramework) ecosystem.
|
|
38
|
+
|
|
39
|
+
It provides a small, dependency-light foundation for:
|
|
40
|
+
|
|
41
|
+
- parsing and validating `SKILL.md` frontmatter and instructions
|
|
42
|
+
- discovering skills on disk (progressive disclosure: metadata first)
|
|
43
|
+
- computing stable content hashes for skill evolution and replay safety
|
|
44
|
+
- formatting compact `<available_skills>` prompt blocks for hosts and agents
|
|
45
|
+
|
|
46
|
+
Flows run; skills are activated. AbstractSkill owns the portable skill contract so `abstractruntime`,
|
|
47
|
+
`abstractgateway`, and thin clients can share identical semantics without duplicating parsers.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install abstractskill
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
|
|
60
|
+
from abstractskill import FilesystemSkillLoader, format_available_skills_xml, parse_skill_md
|
|
61
|
+
|
|
62
|
+
# Parse a SKILL.md file
|
|
63
|
+
doc = parse_skill_md(Path("my-skill/SKILL.md").read_text(encoding="utf-8"))
|
|
64
|
+
print(doc.metadata.name, doc.metadata.description)
|
|
65
|
+
|
|
66
|
+
# Discover skills under one or more roots (later roots override earlier ones)
|
|
67
|
+
loader = FilesystemSkillLoader([Path.home() / ".abstract" / "skills", Path(".abstract/skills")])
|
|
68
|
+
skills = loader.discover()
|
|
69
|
+
print(format_available_skills_xml(skills))
|
|
70
|
+
|
|
71
|
+
# Load full instructions when a skill is activated
|
|
72
|
+
loaded = loader.load("my-skill")
|
|
73
|
+
print(loaded.document.content_hash)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Package scope (v0.1.0)
|
|
77
|
+
|
|
78
|
+
- `parse_skill_md` — YAML frontmatter + markdown body
|
|
79
|
+
- `FilesystemSkillLoader` — list metadata and load full documents
|
|
80
|
+
- `content_hash` — SHA-256 digest for evolution tracking
|
|
81
|
+
- `format_available_skills_xml` — deterministic discovery prompt block
|
|
82
|
+
|
|
83
|
+
Out of scope for this release: gateway registry APIs, zip `.skill` packaging, and runtime activation handlers.
|
|
84
|
+
Those layers live in `abstractgateway` and `abstractruntime` and consume this library.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python -m pip install -e ".[test]"
|
|
90
|
+
python -m pytest -q
|
|
91
|
+
python -m build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
abstractskill/__init__.py,sha256=Zs5siT-k1cqlcCpAnDwNCHo8xySrCYyD91G8Fkqww5g,948
|
|
2
|
+
abstractskill/errors.py,sha256=I3e8uYZ8kOvczbFERNf6fz3vf0hTubLL85wti0JKktw,422
|
|
3
|
+
abstractskill/hash.py,sha256=U2jfSgD1VxnP_wvNiiwhfRfrjf_rYGuSwik0JEXN7ZM,380
|
|
4
|
+
abstractskill/loader.py,sha256=3bIWyhQ8KPsqb-BZBL5hPTgY7tBwIqx51E3hWzaFpiQ,2940
|
|
5
|
+
abstractskill/models.py,sha256=0q87x3lkvfGawFKX0Bt0D9beyQnoRJkeGbv-BZkulB8,1662
|
|
6
|
+
abstractskill/parser.py,sha256=ZhSnz6xnZBdbHq0tcNphNBz0XzLMhhh_O3tFkwAVqJ0,3967
|
|
7
|
+
abstractskill/prompt.py,sha256=wqHvIczlh9xNlkyvjYReK5RRLDexIRSs5xuJY9Czq2E,766
|
|
8
|
+
abstractskill/validation.py,sha256=2ZX-DlihZnKWMqjudemSB2CQlNAXp7vxdFqiyy1sTac,1153
|
|
9
|
+
abstractskill-0.1.0.dist-info/licenses/LICENSE,sha256=z4XcvE7hBF5qzbKZSumgg2ha3EO1Fsd06a34e7wnmXM,1105
|
|
10
|
+
abstractskill-0.1.0.dist-info/METADATA,sha256=AMzYrtIsuE0Ale9mijie37QysvetZ9tZdyYZ2IW3z-k,3420
|
|
11
|
+
abstractskill-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
abstractskill-0.1.0.dist-info/top_level.txt,sha256=GR2MEbgAXsbQcdZusmHcaRv_JpWjyeYbLGzMTHfGQhY,14
|
|
13
|
+
abstractskill-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
abstractskill
|