llmrix-skill 0.2.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.
llmrix/__init__.py ADDED
File without changes
@@ -0,0 +1,70 @@
1
+ """
2
+ llmrix.skill — Git-backed Skill management library.
3
+
4
+ Package layout:
5
+ core/ Cross-cutting concerns (config, exceptions, utils)
6
+ models/ Domain models and data schemas
7
+ git/ Low-level Git repository driver
8
+ storage/ Abstract storage interface + concrete adapters
9
+ services/ High-level orchestration (sync, publish, manage)
10
+ """
11
+ # --- Core ---
12
+ from .core.config import SkillConfig
13
+ from .core.exceptions import (
14
+ GitSkillError,
15
+ SkillNotFoundError,
16
+ VersionNotFoundError,
17
+ PermissionDeniedError,
18
+ ValidationError,
19
+ GitOperationError,
20
+ )
21
+ from .core.utils import build_file_tree
22
+ from .core.plugin import BaseSkill
23
+
24
+ # --- Models ---
25
+ from .models.schema import Skill, SkillVersion
26
+ from .models.metadata import MetadataParser
27
+
28
+ # --- Git ---
29
+ from .git.repository import GitRepository
30
+
31
+ # --- Storage ---
32
+ from .storage.base import BaseStorage
33
+ from .storage.mysql import MySQLStorage
34
+ from .storage.sqlalchemy_store import SQLAlchemyStorage
35
+
36
+ # --- Services ---
37
+ from .services.manager import GitSkillManager
38
+ from .services.publisher import SkillPublisher
39
+ from .services.syncer import SkillSyncer, RemoteSource, SyncResult
40
+
41
+ __version__ = "0.2.0"
42
+
43
+ __all__ = [
44
+ # Core
45
+ "SkillConfig",
46
+ "GitSkillError",
47
+ "SkillNotFoundError",
48
+ "VersionNotFoundError",
49
+ "PermissionDeniedError",
50
+ "ValidationError",
51
+ "GitOperationError",
52
+ "build_file_tree",
53
+ "BaseSkill",
54
+ # Models
55
+ "Skill",
56
+ "SkillVersion",
57
+ "MetadataParser",
58
+ # Git
59
+ "GitRepository",
60
+ # Storage
61
+ "BaseStorage",
62
+ "MySQLStorage",
63
+ "SQLAlchemyStorage",
64
+ # Services
65
+ "GitSkillManager",
66
+ "SkillPublisher",
67
+ "SkillSyncer",
68
+ "RemoteSource",
69
+ "SyncResult",
70
+ ]
@@ -0,0 +1,26 @@
1
+ """
2
+ core: Cross-cutting concerns — configuration, exceptions, and utilities.
3
+ """
4
+ from .config import SkillConfig
5
+ from .exceptions import (
6
+ GitSkillError,
7
+ SkillNotFoundError,
8
+ VersionNotFoundError,
9
+ PermissionDeniedError,
10
+ ValidationError,
11
+ GitOperationError,
12
+ )
13
+ from .utils import build_file_tree
14
+ from .plugin import BaseSkill
15
+
16
+ __all__ = [
17
+ "SkillConfig",
18
+ "GitSkillError",
19
+ "SkillNotFoundError",
20
+ "VersionNotFoundError",
21
+ "PermissionDeniedError",
22
+ "ValidationError",
23
+ "GitOperationError",
24
+ "build_file_tree",
25
+ "BaseSkill",
26
+ ]
@@ -0,0 +1,36 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+
5
+ @dataclass
6
+ class SkillConfig:
7
+ """Configuration for the Skill management system."""
8
+ repo_url: str
9
+ workspace: str
10
+ branch: str = "main"
11
+ skills_path: str = "skills"
12
+ cache_ttl: int = 300
13
+
14
+ @classmethod
15
+ def create(
16
+ cls,
17
+ repo_url: str,
18
+ workspace: Optional[str] = None,
19
+ branch: str = "main"
20
+ ) -> "SkillConfig":
21
+ if not workspace:
22
+ workspace = os.path.expanduser("~/llmrix/skills/remote")
23
+
24
+ return cls(
25
+ repo_url=repo_url,
26
+ workspace=os.path.abspath(os.path.expanduser(workspace)),
27
+ branch=branch
28
+ )
29
+
30
+ @property
31
+ def cache_root(self) -> str:
32
+ return os.path.dirname(self.workspace)
33
+
34
+ @property
35
+ def skills_root(self) -> str:
36
+ return os.path.join(self.workspace, self.skills_path)
@@ -0,0 +1,23 @@
1
+ class GitSkillError(Exception):
2
+ """Base exception for gitskill library"""
3
+ pass
4
+
5
+ class SkillNotFoundError(GitSkillError):
6
+ """Raised when a skill is not found in storage"""
7
+ pass
8
+
9
+ class VersionNotFoundError(GitSkillError):
10
+ """Raised when a specific version of a skill is not found"""
11
+ pass
12
+
13
+ class PermissionDeniedError(GitSkillError):
14
+ """Raised when a user lacks permission to modify a skill"""
15
+ pass
16
+
17
+ class ValidationError(GitSkillError):
18
+ """Raised when skill metadata or code is invalid"""
19
+ pass
20
+
21
+ class GitOperationError(GitSkillError):
22
+ """Raised when a git command fails"""
23
+ pass
@@ -0,0 +1,31 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict
3
+
4
+ class BaseSkill(ABC):
5
+ """
6
+ Standard interface contract for LLMRix skills.
7
+
8
+ Plugin developers should inherit from this class in their `main.py`
9
+ to ensure seamless integration with the host Agent framework.
10
+ """
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """The identifier name of the skill."""
15
+ return self.__class__.__name__
16
+
17
+ @abstractmethod
18
+ async def execute(self, **kwargs: Any) -> Any:
19
+ """
20
+ The main execution entry point for the skill.
21
+ All core business logic and external API calls should be implemented here.
22
+ """
23
+ pass
24
+
25
+ def setup(self, context: Dict[str, Any]) -> None:
26
+ """
27
+ Optional initialization hook.
28
+ The Agent framework can pass configuration, credentials, or runtime context
29
+ here before the skill is executed.
30
+ """
31
+ pass
@@ -0,0 +1,38 @@
1
+ import os
2
+ from typing import Any, Dict, List
3
+
4
+ def build_file_tree(base_dir: str, rel_prefix: str = "") -> List[Dict[str, Any]]:
5
+ """
6
+ Recursively builds a nested file tree (directories first, sorted by name).
7
+ Returns structure:
8
+ [
9
+ {"name": "scripts", "path": "scripts", "type": "dir", "children": [...]},
10
+ {"name": "SKILL.md", "path": "SKILL.md", "type": "file", "size": 1024},
11
+ ]
12
+ """
13
+ items: List[Dict[str, Any]] = []
14
+ try:
15
+ entries = sorted(os.scandir(base_dir), key=lambda e: (e.is_file(), e.name))
16
+ except PermissionError:
17
+ return items
18
+
19
+ for entry in entries:
20
+ if entry.name.startswith("."):
21
+ continue
22
+ rel_path = f"{rel_prefix}{entry.name}" if rel_prefix else entry.name
23
+ if entry.is_dir(follow_symlinks=False):
24
+ children = build_file_tree(entry.path, rel_path + "/")
25
+ items.append({
26
+ "name": entry.name,
27
+ "path": rel_path,
28
+ "type": "dir",
29
+ "children": children,
30
+ })
31
+ else:
32
+ items.append({
33
+ "name": entry.name,
34
+ "path": rel_path,
35
+ "type": "file",
36
+ "size": entry.stat().st_size,
37
+ })
38
+ return items
@@ -0,0 +1,6 @@
1
+ """
2
+ git: Low-level Git repository driver.
3
+ """
4
+ from .repository import GitRepository
5
+
6
+ __all__ = ["GitRepository"]
@@ -0,0 +1,109 @@
1
+ import logging
2
+ import os
3
+ import subprocess
4
+ from contextlib import contextmanager
5
+ from typing import Any, Generator, List, Optional
6
+ from llmrix.skill.core.exceptions import GitOperationError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class GitRepository:
11
+ """
12
+ Handles low-level Git operations for skills.
13
+ Focuses on repository maintenance, state transitions, and file management.
14
+ """
15
+
16
+ def __init__(self, root: str, sub_dir: str = "skills"):
17
+ self.root = os.path.abspath(root)
18
+ self.sub_dir = sub_dir
19
+ self._lock_dir = os.path.join(self.root, ".locks")
20
+
21
+ def get_skill_path(self, code: str) -> str:
22
+ """Returns absolute path to a skill directory."""
23
+ return os.path.join(self.root, self.sub_dir, code)
24
+
25
+ def get_relative_path(self, code: str) -> str:
26
+ """Returns relative path to a skill from repo root."""
27
+ return f"{self.sub_dir}/{code}"
28
+
29
+ @contextmanager
30
+ def lock(self, code: str, timeout: int = 30) -> Generator[None, None, None]:
31
+ """Provides a distributed file lock for a specific skill code."""
32
+ try:
33
+ from filelock import FileLock
34
+ os.makedirs(self._lock_dir, exist_ok=True)
35
+ lock = FileLock(os.path.join(self._lock_dir, f"{code}.lock"), timeout=timeout)
36
+ with lock:
37
+ yield
38
+ except ImportError:
39
+ logger.warning("filelock not installed, running without concurrency protection")
40
+ yield
41
+
42
+ def _execute(self, command: List[str]) -> str:
43
+ """Executes a git command and returns the output."""
44
+ try:
45
+ result = subprocess.run(
46
+ ["git"] + command,
47
+ cwd=self.root,
48
+ stdout=subprocess.PIPE,
49
+ stderr=subprocess.PIPE,
50
+ text=True,
51
+ check=True
52
+ )
53
+ return result.stdout.strip()
54
+ except subprocess.CalledProcessError as e:
55
+ error_msg = (e.stdout + "\\n" + e.stderr).strip()
56
+ raise GitOperationError(f"Git command {' '.join(command)} failed: {error_msg}") from e
57
+
58
+ def ensure_initialized(self, remote_url: Optional[str] = None, branch: str = "main"):
59
+ """Ensures the repository is initialized and tracking the correct branch."""
60
+ if not os.path.isdir(os.path.join(self.root, ".git")):
61
+ if remote_url:
62
+ logger.info(f"Cloning skill repository from {remote_url} (branch: {branch})")
63
+ os.makedirs(os.path.dirname(self.root), exist_ok=True)
64
+ subprocess.run(["git", "clone", "--branch", branch, remote_url, self.root], check=True)
65
+ else:
66
+ logger.info(f"Initializing local skill repository (branch: {branch})")
67
+ os.makedirs(self.root, exist_ok=True)
68
+ self._execute(["init", "-b", branch])
69
+
70
+ # Ensure skills directory exists
71
+ os.makedirs(os.path.join(self.root, self.sub_dir), exist_ok=True)
72
+
73
+ def fetch_latest(self, remote: str = "origin", branch: str = "main"):
74
+ """Pulls the latest changes from the remote repository."""
75
+ try:
76
+ self._execute(["pull", remote, branch])
77
+ except GitOperationError as e:
78
+ logger.warning(f"Git pull failed, falling back to local state: {e}")
79
+
80
+ def push_changes(self, remote: str = "origin", branch: str = "main"):
81
+ """Pushes local commits to the remote repository."""
82
+ try:
83
+ self._execute(["push", remote, branch])
84
+ except GitOperationError as e:
85
+ logger.warning(f"Git push failed: {e}")
86
+
87
+ def commit_skill(self, code: str, author_id: Any, message: Optional[str] = None) -> str:
88
+ """Adds and commits changes for a specific skill."""
89
+ rel_path = self.get_relative_path(code)
90
+ self._execute(["add", rel_path])
91
+
92
+ # Check for staged changes
93
+ diff = self._execute(["diff", "--cached", "--name-only"])
94
+ if not diff:
95
+ return self._execute(["rev-parse", "HEAD"])
96
+
97
+ msg = message or f"Update skill: {code} by user: {author_id}"
98
+ self._execute(["commit", "-m", msg])
99
+ return self._execute(["rev-parse", "HEAD"])
100
+
101
+ def revert_to_commit(self, code: str, commit_hash: str, author_id: Any, message: Optional[str] = None) -> str:
102
+ """Checkouts a specific commit for a skill and commits the reversal."""
103
+ rel_path = self.get_relative_path(code)
104
+ self._execute(["checkout", commit_hash, "--", rel_path])
105
+ self._execute(["add", rel_path])
106
+
107
+ msg = message or f"Revert skill: {code} to {commit_hash} by user: {author_id}"
108
+ self._execute(["commit", "-m", msg])
109
+ return self._execute(["rev-parse", "HEAD"])
@@ -0,0 +1,7 @@
1
+ """
2
+ models: Domain models and data schemas.
3
+ """
4
+ from .schema import Skill, SkillVersion
5
+ from .metadata import MetadataParser
6
+
7
+ __all__ = ["Skill", "SkillVersion", "MetadataParser"]
@@ -0,0 +1,46 @@
1
+ import yaml
2
+ import re
3
+ from typing import Any, Dict, Optional
4
+ from llmrix.skill.core.exceptions import ValidationError
5
+
6
+ class MetadataParser:
7
+ """Parses skill files and validates codes."""
8
+
9
+ @staticmethod
10
+ def parse_manifest(content: str) -> Dict[str, Any]:
11
+ """Extracts frontmatter from SKILL.md."""
12
+ if not content.startswith("---"):
13
+ return {}
14
+ try:
15
+ match = re.match(r"^---\s*\\n(.*?)\\n---\s*\\n", content, re.DOTALL)
16
+ if match:
17
+ return yaml.safe_load(match.group(1)) or {}
18
+ except Exception as e:
19
+ raise ValidationError(f"Failed to parse metadata frontmatter: {e}") from e
20
+ return {}
21
+
22
+ @staticmethod
23
+ def validate_code(code: str) -> None:
24
+ """Ensures skill code follows naming conventions."""
25
+ if not (code and re.match(r"^[a-z0-9_\\-]+$", code)):
26
+ raise ValidationError(
27
+ f"Invalid skill code '{code}'. Only lowercase, numbers, - and _ are allowed."
28
+ )
29
+
30
+ @staticmethod
31
+ def detect_category(code: str, name: str, description: str) -> str:
32
+ """Heuristically detects skill category based on keywords."""
33
+ text = " ".join([code, name or "", description or ""]).lower()
34
+
35
+ KEYWORDS = [
36
+ ("Developer Tools", ["git", "code", "python", "javascript", "typescript", "compiler", "debug", "ide", "sdk", "api"]),
37
+ ("Data Analytics", ["data", "sql", "database", "excel", "csv", "chart", "analytics", "bi", "统计", "数据", "分析"]),
38
+ ("Content Creation", ["image", "photo", "video", "audio", "canvas", "design", "content", "图片", "图像", "设计"]),
39
+ ("Web & Search", ["search", "web", "browser", "crawl", "scrape", "http", "url", "搜索", "网页"]),
40
+ ("System Integration", ["shell", "bash", "deploy", "docker", "k8s", "ci", "cd", "monitor", "email", "notify", "自动化", "集成"]),
41
+ ]
42
+
43
+ for category, keywords in KEYWORDS:
44
+ if any(kw in text for kw in keywords):
45
+ return category
46
+ return "Other"
@@ -0,0 +1,27 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime
3
+ from typing import Any, Dict, Optional
4
+
5
+ @dataclass
6
+ class Skill:
7
+ """Represents the current state of a skill."""
8
+ code: str
9
+ name: str
10
+ version: int = 1
11
+ description: Optional[str] = None
12
+ category: str = "Other"
13
+ commit_hash: Optional[str] = None
14
+ file_path: Optional[str] = None
15
+ status: int = 0
16
+ metadata: Dict[str, Any] = field(default_factory=dict)
17
+
18
+ @dataclass
19
+ class SkillVersion:
20
+ """Represents a specific point-in-time version of a skill."""
21
+ code: str
22
+ version: int
23
+ commit_hash: str
24
+ author_id: Any
25
+ file_path: Optional[str] = None
26
+ message: Optional[str] = None
27
+ created_at: datetime = field(default_factory=datetime.now)
@@ -0,0 +1,14 @@
1
+ """
2
+ services: High-level business logic — sync, publish, and orchestration.
3
+ """
4
+ from .manager import GitSkillManager
5
+ from .publisher import SkillPublisher
6
+ from .syncer import SkillSyncer, RemoteSource, SyncResult
7
+
8
+ __all__ = [
9
+ "GitSkillManager",
10
+ "SkillPublisher",
11
+ "SkillSyncer",
12
+ "RemoteSource",
13
+ "SyncResult",
14
+ ]
@@ -0,0 +1,98 @@
1
+ import os
2
+ import logging
3
+ from typing import Any, Optional, List
4
+ from llmrix.skill.git.repository import GitRepository
5
+ from llmrix.skill.storage.base import BaseStorage
6
+ from llmrix.skill.models.metadata import MetadataParser
7
+ from llmrix.skill.services.syncer import SkillSyncer
8
+ from llmrix.skill.services.publisher import SkillPublisher
9
+ from llmrix.skill.core.config import SkillConfig
10
+ from llmrix.skill.models.schema import Skill, SkillVersion
11
+ from llmrix.skill.core.exceptions import GitSkillError
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class GitSkillManager:
16
+ """
17
+ Converged Orchestrator for SkillHub.
18
+
19
+ Acts as a high-level facade that decouples configuration from method calls,
20
+ providing a clean interface for both Agent Workers and Management APIs.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ repo_url: str,
26
+ workspace: Optional[str] = None,
27
+ branch: str = "main",
28
+ storage: Optional[BaseStorage] = None
29
+ ):
30
+ # 1. Decouple Configuration
31
+ self.config = SkillConfig.create(repo_url, workspace, branch)
32
+ self.storage = storage
33
+
34
+ # 2. Initialize Internal Components
35
+ self.repository = GitRepository(root=self.config.workspace, sub_dir=self.config.skills_path)
36
+ self.parser = MetadataParser()
37
+
38
+ # 3. Read Responsibility (Syncing)
39
+ self.syncer = SkillSyncer(cache_dir=self.config.cache_root)
40
+
41
+ # 4. Write Responsibility (Publishing - Lazy)
42
+ self._publisher = None
43
+ if storage:
44
+ self._publisher = SkillPublisher(
45
+ git=self.repository,
46
+ storage=storage,
47
+ parser=self.parser,
48
+ default_branch=self.config.branch
49
+ )
50
+
51
+ @property
52
+ def publisher(self) -> SkillPublisher:
53
+ if not self._publisher:
54
+ raise GitSkillError("Storage adapter is required for publishing operations.")
55
+ return self._publisher
56
+
57
+ def sync(self) -> str:
58
+ """
59
+ Worker Mode API: Synchronizes the local repository.
60
+ Uses the pre-configured branch and workspace.
61
+ """
62
+ self.repository.ensure_initialized(
63
+ remote_url=self.config.repo_url,
64
+ branch=self.config.branch
65
+ )
66
+ self.repository.fetch_latest(branch=self.config.branch)
67
+ return self.repository.get_skill_path("")
68
+
69
+ def publish(self, **kwargs) -> Skill:
70
+ """
71
+ Management Mode API: Deploys a new version.
72
+ Arguments like 'branch' are optional as the manager uses defaults.
73
+ """
74
+ self.repository.ensure_initialized(
75
+ remote_url=self.config.repo_url,
76
+ branch=self.config.branch
77
+ )
78
+ return self.publisher.publish(**kwargs)
79
+
80
+ def rollback(self, **kwargs) -> Skill:
81
+ """Management Mode API: Reverts to a previous version."""
82
+ self.repository.ensure_initialized(
83
+ remote_url=self.config.repo_url,
84
+ branch=self.config.branch
85
+ )
86
+ return self.publisher.rollback(**kwargs)
87
+
88
+ def get_history(self, code: str) -> List[SkillVersion]:
89
+ """Retrieves release history from the connected database."""
90
+ if not self.storage:
91
+ raise GitSkillError("Storage adapter is required for history queries.")
92
+ return self.storage.get_history(code)
93
+
94
+ @staticmethod
95
+ def get_interim_path(uid: Any) -> str:
96
+ """Static utility to resolve a standardized interim upload path for users."""
97
+ base = os.path.expanduser(f"~/llmrix/skills/interim/{uid}")
98
+ return os.path.abspath(base)
@@ -0,0 +1,151 @@
1
+ import os
2
+ import shutil
3
+ import logging
4
+ from typing import Any, Optional
5
+ from llmrix.skill.git.repository import GitRepository
6
+ from llmrix.skill.storage.base import BaseStorage
7
+ from llmrix.skill.models.metadata import MetadataParser
8
+ from llmrix.skill.models.schema import Skill, SkillVersion
9
+ from llmrix.skill.core.exceptions import PermissionDeniedError, VersionNotFoundError
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class SkillPublisher:
14
+ """
15
+ Handles the deployment lifecycle of Skills.
16
+ Encapsulates permissions, filesystem updates, and persistence.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ git: GitRepository,
22
+ storage: BaseStorage,
23
+ parser: Optional[MetadataParser] = None,
24
+ default_branch: str = "main"
25
+ ):
26
+ self.git = git
27
+ self.storage = storage
28
+ self.parser = parser or MetadataParser()
29
+ self.default_branch = default_branch
30
+
31
+ def publish(
32
+ self,
33
+ code: str,
34
+ source_dir: str,
35
+ user_id: Any,
36
+ name: Optional[str] = None,
37
+ description: Optional[str] = None,
38
+ category: Optional[str] = None,
39
+ message: Optional[str] = None,
40
+ branch: Optional[str] = None
41
+ ) -> Skill:
42
+ """Executes a full deployment workflow for a skill."""
43
+ target_branch = branch or self.default_branch
44
+ self.parser.validate_code(code)
45
+
46
+ with self.git.lock(code):
47
+ self.git.fetch_latest(branch=target_branch)
48
+
49
+ # 1. Authorization
50
+ existing = self.storage.get_skill(code)
51
+ if existing and not self.storage.can_modify(code, user_id):
52
+ raise PermissionDeniedError(f"User {user_id} lacks permission for skill {code}")
53
+
54
+ # 2. Filesystem Update
55
+ target_path = self.git.get_skill_path(code)
56
+ if os.path.exists(target_path):
57
+ shutil.rmtree(target_path)
58
+ shutil.copytree(source_dir, target_path)
59
+
60
+ # 3. Metadata Extraction
61
+ manifest_path = os.path.join(target_path, "SKILL.md")
62
+ manifest = {}
63
+ if os.path.exists(manifest_path):
64
+ with open(manifest_path, "r", encoding="utf-8") as f:
65
+ manifest = self.parser.parse_manifest(f.read())
66
+
67
+ final_name = name or manifest.get("name") or code
68
+ final_desc = description or manifest.get("description")
69
+ final_cat = (
70
+ category
71
+ or manifest.get("category")
72
+ or self.parser.detect_category(code, final_name, final_desc or "")
73
+ )
74
+
75
+ # 4. Git Orchestration
76
+ commit_hash = self.git.commit_skill(code, user_id, message)
77
+ self.git.push_changes(branch=target_branch)
78
+
79
+ # 5. Database Persistence
80
+ new_version_num = (existing.version + 1) if existing else 1
81
+ skill = Skill(
82
+ code=code,
83
+ name=final_name,
84
+ version=new_version_num,
85
+ description=final_desc,
86
+ category=final_cat,
87
+ commit_hash=commit_hash,
88
+ file_path=self.git.get_relative_path(code),
89
+ status=0
90
+ )
91
+
92
+ self.storage.save_skill(skill)
93
+ self.storage.add_version(SkillVersion(
94
+ code=code,
95
+ version=new_version_num,
96
+ commit_hash=commit_hash,
97
+ author_id=user_id,
98
+ file_path=skill.file_path,
99
+ message=message
100
+ ))
101
+
102
+ return skill
103
+
104
+ def rollback(
105
+ self,
106
+ code: str,
107
+ target_version: int,
108
+ user_id: Any,
109
+ message: Optional[str] = None,
110
+ branch: Optional[str] = None
111
+ ) -> Skill:
112
+ """Rolls back a skill to a specific historical version."""
113
+ target_branch = branch or self.default_branch
114
+ ver = self.storage.get_version(code, target_version)
115
+ if not ver:
116
+ raise VersionNotFoundError(f"Version {target_version} not found for {code}")
117
+
118
+ with self.git.lock(code):
119
+ self.git.fetch_latest(branch=target_branch)
120
+
121
+ if not self.storage.can_modify(code, user_id):
122
+ raise PermissionDeniedError(f"Access denied for skill {code}")
123
+
124
+ new_hash = self.git.revert_to_commit(code, ver.commit_hash, user_id, message)
125
+ self.git.push_changes(branch=target_branch)
126
+
127
+ existing = self.storage.get_skill(code)
128
+ new_version_num = (existing.version + 1) if existing else 1
129
+
130
+ skill = Skill(
131
+ code=code,
132
+ name=existing.name,
133
+ version=new_version_num,
134
+ description=existing.description,
135
+ category=existing.category,
136
+ commit_hash=new_hash,
137
+ file_path=existing.file_path,
138
+ status=existing.status
139
+ )
140
+
141
+ self.storage.save_skill(skill)
142
+ self.storage.add_version(SkillVersion(
143
+ code=code,
144
+ version=new_version_num,
145
+ commit_hash=new_hash,
146
+ author_id=user_id,
147
+ file_path=skill.file_path,
148
+ message=message or f"Rollback to v{target_version}"
149
+ ))
150
+
151
+ return skill
@@ -0,0 +1,121 @@
1
+ import asyncio
2
+ import hashlib
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import urllib.parse
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, timedelta
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+ from llmrix.skill.git.repository import GitRepository
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ @dataclass
16
+ class RemoteSource:
17
+ """Configuration for a remote skill repository source."""
18
+ url: str
19
+ branch: str = "main"
20
+ ssh_key: Optional[str] = None
21
+ username: Optional[str] = None
22
+ password: Optional[str] = None
23
+ cache_ttl: int = 300 # seconds
24
+ skills_path: str = "skills"
25
+
26
+ @property
27
+ def identity(self) -> str:
28
+ """Unique identifier based on URL and branch."""
29
+ return f"{self.url}:{self.branch}"
30
+
31
+ @dataclass
32
+ class SyncResult:
33
+ """Result of a repository sync operation."""
34
+ source: RemoteSource
35
+ local_path: Path
36
+ success: bool
37
+ last_sync: datetime
38
+ error: Optional[str] = None
39
+
40
+ class SkillSyncer:
41
+ """
42
+ Manages synchronization of multiple remote skill repositories.
43
+ Supports local caching with TTL and authentication.
44
+ """
45
+
46
+ def __init__(self, cache_dir: str):
47
+ self.cache_dir = Path(cache_dir).absolute()
48
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
49
+ self._locks: Dict[str, asyncio.Lock] = {}
50
+ self._sync_history: Dict[str, datetime] = {}
51
+
52
+ def _get_lock(self, identity: str) -> asyncio.Lock:
53
+ if identity not in self._locks:
54
+ self._locks[identity] = asyncio.Lock()
55
+ return self._locks[identity]
56
+
57
+ def _get_cache_path(self, source: RemoteSource) -> Path:
58
+ """Generates a unique directory name for the repository hash."""
59
+ url_hash = hashlib.md5(source.identity.encode()).hexdigest()[:12]
60
+ return self.cache_dir / f"repo_{url_hash}"
61
+
62
+ def _build_authed_url(self, source: RemoteSource) -> str:
63
+ url = source.url
64
+ if source.username and source.password and url.startswith("https://"):
65
+ encoded_user = urllib.parse.quote(source.username, safe="")
66
+ encoded_pass = urllib.parse.quote(source.password, safe="")
67
+ url = url.replace("https://", f"https://{encoded_user}:{encoded_pass}@")
68
+ return url
69
+
70
+ async def sync_source(self, source: RemoteSource) -> SyncResult:
71
+ """
72
+ Synchronizes a single remote source.
73
+ Returns the path to the skills directory within the repo.
74
+ """
75
+ identity = source.identity
76
+ lock = self._get_lock(identity)
77
+
78
+ async with lock:
79
+ repo_path = self._get_cache_path(source)
80
+ repo = GitRepository(root=str(repo_path), sub_dir=source.skills_path)
81
+
82
+ # Check TTL
83
+ last_sync = self._sync_history.get(identity)
84
+ if last_sync and (datetime.now() - last_sync).total_seconds() < source.cache_ttl:
85
+ if repo_path.exists():
86
+ return SyncResult(source, Path(repo.get_skill_path("")), True, last_sync)
87
+
88
+ try:
89
+ authed_url = self._build_authed_url(source)
90
+
91
+ # We use a thread pool for blocking git operations since they are subprocess calls
92
+ loop = asyncio.get_event_loop()
93
+
94
+ if not repo_path.exists():
95
+ logger.info(f"Cloning remote skill source: {source.url}")
96
+ await loop.run_in_executor(None, repo.initialize, authed_url, source.branch)
97
+ else:
98
+ logger.debug(f"Syncing remote skill source: {source.url}")
99
+ await loop.run_in_executor(None, repo.sync, "origin", source.branch)
100
+
101
+ self._sync_history[identity] = datetime.now()
102
+ return SyncResult(source, Path(repo.get_skill_path("")), True, self._sync_history[identity])
103
+
104
+ except Exception as e:
105
+ logger.error(f"Failed to sync {source.url}: {e}")
106
+ if repo_path.exists():
107
+ logger.warning(f"Falling back to stale cache for {source.url}")
108
+ return SyncResult(source, Path(repo.get_skill_path("")), True, last_sync or datetime.min, str(e))
109
+ return SyncResult(source, repo_path, False, datetime.min, str(e))
110
+
111
+ async def sync_all(self, sources: List[RemoteSource]) -> List[SyncResult]:
112
+ """Synchronizes multiple sources in parallel."""
113
+ tasks = [self.sync_source(s) for s in sources]
114
+ return await asyncio.gather(*tasks)
115
+
116
+ def clear_cache(self):
117
+ """Deletes all cached repositories."""
118
+ if self.cache_dir.exists():
119
+ shutil.rmtree(self.cache_dir)
120
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
121
+ self._sync_history.clear()
@@ -0,0 +1,8 @@
1
+ """
2
+ storage: Abstract storage interface and concrete adapter implementations.
3
+ """
4
+ from .base import BaseStorage
5
+ from .mysql import MySQLStorage
6
+ from .sqlalchemy_store import SQLAlchemyStorage
7
+
8
+ __all__ = ["BaseStorage", "MySQLStorage", "SQLAlchemyStorage"]
@@ -0,0 +1,39 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, List, Optional
3
+ from llmrix.skill.models.schema import Skill, SkillVersion
4
+
5
+ class BaseStorage(ABC):
6
+ """
7
+ Abstract base class for skill persistence.
8
+ Implement this to support different databases.
9
+ """
10
+
11
+ @abstractmethod
12
+ def get_skill(self, code: str) -> Optional[Skill]:
13
+ """Retrieve skill by its unique code."""
14
+ pass
15
+
16
+ @abstractmethod
17
+ def save_skill(self, skill: Skill) -> None:
18
+ """Create or update skill metadata."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def add_version(self, version: SkillVersion) -> None:
23
+ """Record a new version in history."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ def get_history(self, code: str) -> List[SkillVersion]:
28
+ """List all versions of a skill ordered by version descending."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def get_version(self, code: str, version_number: int) -> Optional[SkillVersion]:
33
+ """Retrieve a specific version record."""
34
+ pass
35
+
36
+ @abstractmethod
37
+ def can_modify(self, code: str, user_id: Any) -> bool:
38
+ """Check if the user has permission to modify the skill."""
39
+ pass
@@ -0,0 +1,100 @@
1
+ from typing import Any, List, Optional
2
+ from llmrix.skill.storage.base import BaseStorage
3
+ from llmrix.skill.models.schema import Skill, SkillVersion
4
+
5
+ class MySQLStorage(BaseStorage):
6
+ def __init__(self, connection_factory):
7
+ self.get_conn = connection_factory
8
+
9
+ def _to_skill(self, row: dict) -> Skill:
10
+ return Skill(
11
+ code=row["skill_code"],
12
+ name=row["skill_name"],
13
+ version=row["version"],
14
+ description=row["introduce"],
15
+ category=row["category"],
16
+ commit_hash=row["git_commit"],
17
+ file_path=row["git_path"],
18
+ status=row["status"]
19
+ )
20
+
21
+ def get_skill(self, code: str) -> Optional[Skill]:
22
+ with self.get_conn() as conn:
23
+ cur = conn.cursor()
24
+ cur.execute("SELECT * FROM skill_info WHERE skill_code=%s AND deleted=0", (code,))
25
+ row = cur.fetchone()
26
+ return self._to_skill(row) if row else None
27
+
28
+ def save_skill(self, skill: Skill) -> None:
29
+ sql = """
30
+ INSERT INTO skill_info (skill_code, skill_name, introduce, version, git_commit, git_path, status, category)
31
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
32
+ ON DUPLICATE KEY UPDATE
33
+ skill_name = VALUES(skill_name),
34
+ introduce = VALUES(introduce),
35
+ version = VALUES(version),
36
+ git_commit = VALUES(git_commit),
37
+ git_path = VALUES(git_path),
38
+ category = VALUES(category),
39
+ update_time = NOW()
40
+ """
41
+ with self.get_conn() as conn:
42
+ cur = conn.cursor()
43
+ cur.execute(sql, (
44
+ skill.code, skill.name, skill.description,
45
+ skill.version, skill.commit_hash, skill.file_path,
46
+ skill.status, skill.category
47
+ ))
48
+ conn.commit()
49
+
50
+ def add_version(self, version: SkillVersion) -> None:
51
+ sql = """
52
+ INSERT INTO skill_version (skill_code, version, git_commit, git_path, user_id, remark)
53
+ VALUES (%s, %s, %s, %s, %s, %s)
54
+ """
55
+ with self.get_conn() as conn:
56
+ cur = conn.cursor()
57
+ cur.execute(sql, (
58
+ version.code, version.version, version.commit_hash,
59
+ version.file_path, version.author_id, version.message
60
+ ))
61
+ conn.commit()
62
+
63
+ def get_history(self, code: str) -> List[SkillVersion]:
64
+ with self.get_conn() as conn:
65
+ cur = conn.cursor()
66
+ cur.execute("SELECT * FROM skill_version WHERE skill_code=%s AND deleted=0 ORDER BY version DESC", (code,))
67
+ rows = cur.fetchall()
68
+ return [SkillVersion(
69
+ code=r["skill_code"],
70
+ version=r["version"],
71
+ commit_hash=r["git_commit"],
72
+ author_id=r["user_id"],
73
+ file_path=r["git_path"],
74
+ message=r["remark"],
75
+ created_at=r["create_time"]
76
+ ) for r in rows]
77
+
78
+ def get_version(self, code: str, version_number: int) -> Optional[SkillVersion]:
79
+ with self.get_conn() as conn:
80
+ cur = conn.cursor()
81
+ cur.execute("SELECT * FROM skill_version WHERE skill_code=%s AND version=%s AND deleted=0",
82
+ (code, version_number))
83
+ r = cur.fetchone()
84
+ if not r: return None
85
+ return SkillVersion(
86
+ code=r["skill_code"],
87
+ version=r["version"],
88
+ commit_hash=r["git_commit"],
89
+ author_id=r["user_id"],
90
+ file_path=r["git_path"],
91
+ message=r["remark"],
92
+ created_at=r["create_time"]
93
+ )
94
+
95
+ def can_modify(self, code: str, user_id: Any) -> bool:
96
+ with self.get_conn() as conn:
97
+ cur = conn.cursor()
98
+ cur.execute("SELECT id FROM skill_info WHERE skill_code=%s AND user_id=%s AND deleted=0",
99
+ (code, user_id))
100
+ return cur.fetchone() is not None
@@ -0,0 +1,144 @@
1
+ from typing import Any, List, Optional
2
+ from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime
3
+ from sqlalchemy.orm import declarative_base, sessionmaker
4
+ from datetime import datetime
5
+
6
+ from llmrix.skill.storage.base import BaseStorage
7
+ from llmrix.skill.models.schema import Skill, SkillVersion
8
+
9
+ Base = declarative_base()
10
+
11
+ class SkillModel(Base):
12
+ __tablename__ = "skill_info"
13
+
14
+ id = Column(Integer, primary_key=True, autoincrement=True)
15
+ skill_code = Column(String(100), unique=True, nullable=False, index=True)
16
+ skill_name = Column(String(200), nullable=False)
17
+ introduce = Column(Text, nullable=True) # Description
18
+ category = Column(String(100), nullable=True)
19
+ version = Column(Integer, default=1)
20
+ git_commit = Column(String(40), nullable=False)
21
+ git_path = Column(String(500), nullable=False)
22
+ status = Column(Integer, default=0)
23
+ user_id = Column(String(100), nullable=True) # Author/Owner
24
+ deleted = Column(Integer, default=0)
25
+ create_time = Column(DateTime, default=datetime.utcnow)
26
+ update_time = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
27
+
28
+ class SkillVersionModel(Base):
29
+ __tablename__ = "skill_version"
30
+
31
+ id = Column(Integer, primary_key=True, autoincrement=True)
32
+ skill_code = Column(String(100), nullable=False, index=True)
33
+ version = Column(Integer, nullable=False)
34
+ git_commit = Column(String(40), nullable=False)
35
+ git_path = Column(String(500), nullable=False)
36
+ user_id = Column(String(100), nullable=False)
37
+ remark = Column(Text, nullable=True)
38
+ deleted = Column(Integer, default=0)
39
+ create_time = Column(DateTime, default=datetime.utcnow)
40
+
41
+ class SQLAlchemyStorage(BaseStorage):
42
+ """
43
+ Database agnostic storage using SQLAlchemy ORM.
44
+ Supports MySQL, PostgreSQL, SQLite, etc.
45
+ """
46
+ def __init__(self, engine_or_url: Any, auto_create_tables: bool = True):
47
+ if isinstance(engine_or_url, str):
48
+ self.engine = create_engine(engine_or_url)
49
+ else:
50
+ self.engine = engine_or_url
51
+
52
+ self.SessionLocal = sessionmaker(bind=self.engine)
53
+
54
+ if auto_create_tables:
55
+ Base.metadata.create_all(self.engine)
56
+
57
+ def _to_skill(self, model: SkillModel) -> Skill:
58
+ return Skill(
59
+ code=model.skill_code,
60
+ name=model.skill_name,
61
+ version=model.version,
62
+ description=model.introduce,
63
+ category=model.category,
64
+ commit_hash=model.git_commit,
65
+ file_path=model.git_path,
66
+ status=model.status
67
+ )
68
+
69
+ def get_skill(self, code: str) -> Optional[Skill]:
70
+ with self.SessionLocal() as db:
71
+ model = db.query(SkillModel).filter_by(skill_code=code, deleted=0).first()
72
+ return self._to_skill(model) if model else None
73
+
74
+ def save_skill(self, skill: Skill) -> None:
75
+ with self.SessionLocal() as db:
76
+ model = db.query(SkillModel).filter_by(skill_code=skill.code, deleted=0).first()
77
+ if model:
78
+ model.skill_name = skill.name
79
+ model.introduce = skill.description
80
+ model.version = skill.version
81
+ model.git_commit = skill.commit_hash
82
+ model.git_path = skill.file_path
83
+ model.status = skill.status
84
+ model.category = skill.category
85
+ else:
86
+ model = SkillModel(
87
+ skill_code=skill.code,
88
+ skill_name=skill.name,
89
+ introduce=skill.description,
90
+ version=skill.version,
91
+ git_commit=skill.commit_hash,
92
+ git_path=skill.file_path,
93
+ status=skill.status,
94
+ category=skill.category
95
+ )
96
+ db.add(model)
97
+ db.commit()
98
+
99
+ def add_version(self, version: SkillVersion) -> None:
100
+ with self.SessionLocal() as db:
101
+ model = SkillVersionModel(
102
+ skill_code=version.code,
103
+ version=version.version,
104
+ git_commit=version.commit_hash,
105
+ git_path=version.file_path,
106
+ user_id=str(version.author_id),
107
+ remark=version.message
108
+ )
109
+ db.add(model)
110
+ db.commit()
111
+
112
+ def get_history(self, code: str) -> List[SkillVersion]:
113
+ with self.SessionLocal() as db:
114
+ models = db.query(SkillVersionModel).filter_by(skill_code=code, deleted=0).order_by(SkillVersionModel.version.desc()).all()
115
+ return [
116
+ SkillVersion(
117
+ code=m.skill_code,
118
+ version=m.version,
119
+ commit_hash=m.git_commit,
120
+ author_id=m.user_id,
121
+ file_path=m.git_path,
122
+ message=m.remark,
123
+ created_at=m.create_time
124
+ ) for m in models
125
+ ]
126
+
127
+ def get_version(self, code: str, version_number: int) -> Optional[SkillVersion]:
128
+ with self.SessionLocal() as db:
129
+ m = db.query(SkillVersionModel).filter_by(skill_code=code, version=version_number, deleted=0).first()
130
+ if not m: return None
131
+ return SkillVersion(
132
+ code=m.skill_code,
133
+ version=m.version,
134
+ commit_hash=m.git_commit,
135
+ author_id=m.user_id,
136
+ file_path=m.git_path,
137
+ message=m.remark,
138
+ created_at=m.create_time
139
+ )
140
+
141
+ def can_modify(self, code: str, user_id: Any) -> bool:
142
+ with self.SessionLocal() as db:
143
+ model = db.query(SkillModel).filter_by(skill_code=code, user_id=str(user_id), deleted=0).first()
144
+ return model is not None
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: llmrix-skill
3
+ Version: 0.2.0
4
+ Summary: A professional Git-based Skill management library for LLM agents.
5
+ Author-email: LLMRix Team <support@llmrix.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/llmrix/llmrix-skillhub
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: filelock>=3.0.0
12
+ Requires-Dist: sqlalchemy>=2.0.0
13
+
14
+ # LLMRix Skill Plugin Library
15
+
16
+ `llmrix-skill` is a professional Git-based Skill and plugin management framework designed for LLM Agents. It encapsulates the complex logic of publishing, versioning, and syncing code, prompts, and tools into an out-of-the-box plugin library, allowing developers to focus on building core Agent logic.
17
+
18
+ ## Core Features
19
+
20
+ As the foundational plugin library for an Agent framework, it provides the following capabilities:
21
+
22
+ - **Git-Driven Plugin Market**: Uses Git repositories as the Single Source of Truth (SSOT) to achieve skill persistence, version tracing, and change tracking.
23
+ - **Out-of-the-box Dual Mode Architecture**:
24
+ - **Worker Mode (Execution)**: Pure lightweight synchronization, responsible for pulling skills from remote repositories to local storage for high-speed loading by Agent instances.
25
+ - **Management Mode (Admin)**: Integrates with databases to support multi-tenant authentication, skill publishing, metadata parsing, and version rollbacks.
26
+ - **Dynamic Metadata Parsing**: Automatically parses and validates `SKILL.md` (YAML Frontmatter) in the skill package to extract the plugin name, description, and category.
27
+ - **Concurrency & Safety**: Built-in distributed file lock mechanism to prevent file state race conditions during high-concurrency publishing and pulling.
28
+
29
+ ---
30
+
31
+ ## Quick Usage Guide
32
+
33
+ ### Installation
34
+
35
+ You can integrate this plugin library into your Agent project via pip:
36
+
37
+ ```bash
38
+ pip install llmrix-skill
39
+ ```
40
+
41
+ ### Scene 1: Syncing Plugins on Agent Execution Node (Worker Mode)
42
+
43
+ In your Agent execution environment (such as background task nodes, or upon container startup), configure a specific branch to pull the latest skills for model invocation.
44
+
45
+ ```python
46
+ from llmrix.skill import GitSkillManager
47
+
48
+ def sync_skills_for_agent():
49
+ # Initialize the Manager, configuring the remote repository and target branch
50
+ manager = GitSkillManager(
51
+ repo_url="https://github.com/your-org/skill-repo.git",
52
+ branch="develop", # Highly recommended: specify the branch to pull (e.g., main/develop/v1)
53
+ workspace="/path/to/local/cache" # Local cache path
54
+ )
55
+
56
+ # Execute sync, returning the absolute filesystem path where the skills are located
57
+ skills_path = manager.sync()
58
+ print(f"✅ Skill plugins synced to: {skills_path}")
59
+
60
+ # You can now dynamically load modules under skills_path in your Agent framework
61
+ ```
62
+
63
+ ### Scene 2: Web Server Management & Publishing (Management Mode)
64
+
65
+ In your Web API service (such as FastAPI/Django plugin market backend), use `GitSkillManager` to handle user uploads, publishing, and version rollbacks. It encapsulates database persistence and concurrent file locking.
66
+
67
+ ```python
68
+ from llmrix.skill import GitSkillManager
69
+ from llmrix.skill import MySQLStorage
70
+
71
+ # 1. Configure the database adapter
72
+ def get_db_connection():
73
+ # Return a pymysql/MySQLdb connection object
74
+ pass
75
+
76
+ storage = MySQLStorage(connection_factory=get_db_connection)
77
+
78
+ # 2. Initialize the Manager, dedicated for the management end
79
+ manager = GitSkillManager(
80
+ repo_url="git@github.com:your-org/skill-repo.git",
81
+ storage=storage
82
+ )
83
+
84
+ # 3. Publish user-uploaded skills
85
+ def publish_user_skill(user_id, uploaded_dir):
86
+ skill = manager.publish(
87
+ code="python_interpreter", # Unique skill code
88
+ source_dir=uploaded_dir, # Unzipped directory uploaded by the user
89
+ user_id=user_id, # Current operating user ID (for auth)
90
+ message="Initial release"
91
+ )
92
+ print(f"🚀 Publish successful: {skill.name} v{skill.version}")
93
+
94
+ # 4. Version Rollback
95
+ def rollback_skill(user_id):
96
+ skill = manager.rollback(
97
+ code="python_interpreter",
98
+ target_version=1,
99
+ user_id=user_id,
100
+ message="Revert due to bugs"
101
+ )
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Skill Package Specification
107
+
108
+ A standard skill plugin package is a directory containing the following files:
109
+
110
+ ```text
111
+ my_awesome_skill/
112
+ ├── SKILL.md # Skill description and metadata (Required)
113
+ ├── main.py # Core logic (Recommended)
114
+ └── requirements.txt # Dependencies (Optional)
115
+ ```
116
+
117
+ `SKILL.md` must contain a valid YAML Frontmatter header:
118
+
119
+ ```markdown
120
+ ---
121
+ name: Web Scraper Pro
122
+ description: Powerful web scraping tool, supports dynamic rendering.
123
+ category: Web & Search
124
+ ---
125
+
126
+ Detailed Markdown documentation about this skill goes here...
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Module Architecture
132
+
133
+ You can directly import the required submodules for lower-level extensions:
134
+
135
+ - `llmrix.skill.services`: Contains `GitSkillManager`, `SkillPublisher`, and `SkillSyncer`.
136
+ - `llmrix.skill.storage`: Contains `BaseStorage` and `MySQLStorage`. Inherit `BaseStorage` to implement your own MongoDB or PostgreSQL adapters.
137
+ - `llmrix.skill.git`: Low-level Git driver library `GitRepository`.
138
+ - `llmrix.skill.models`: Data models `Skill` and `SkillVersion`.
@@ -0,0 +1,24 @@
1
+ llmrix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ llmrix/skill/__init__.py,sha256=2bTh79cvfDE6YJOutPbG6FwR1VD2LUulXV9ehq9fr1s,1707
3
+ llmrix/skill/core/__init__.py,sha256=9ugCN6KmPJLWdEeAx7OV_vdPSAUXRw1ownLaOvCSf3w,574
4
+ llmrix/skill/core/config.py,sha256=gdS5hZQ7AT_Fu7oVgDgQOF8DKbmTToreALCTdGbLlEE,913
5
+ llmrix/skill/core/exceptions.py,sha256=gVTrBNw0vo1FwE8B05i8fGcfCKlI7i3FFEyzi2d2dw4,624
6
+ llmrix/skill/core/plugin.py,sha256=y2yB4dHumRML9dvLthb_S9w-ItsfthfsK7A6DWKmSkQ,944
7
+ llmrix/skill/core/utils.py,sha256=Jn_Azb_LEzDMkb0PQnoaGMUzr-CnsDWmJLfhhnJsRzU,1296
8
+ llmrix/skill/git/__init__.py,sha256=nB06TtVJV5-HPlQ1GwSEBOH793KNyf9npn-QSJuZyUU,113
9
+ llmrix/skill/git/repository.py,sha256=UaD1LmHSOITO-TImjCoET6p1cl0NwAG22P_2IDnGHVE,4748
10
+ llmrix/skill/models/__init__.py,sha256=HowL3n_j1ecwRpRj8Q72zpYFWKIolIpocGtyZ3JQCsA,180
11
+ llmrix/skill/models/metadata.py,sha256=6F3pCZflZ-bOVwTBqya1Rz1HpPUfrorh4gjzYwuGbEs,2101
12
+ llmrix/skill/models/schema.py,sha256=Eaa29jwH7_Eu0FqzQQaDXOZ0NLs_04PCtMRp3_qDX5A,756
13
+ llmrix/skill/services/__init__.py,sha256=4Qu266WVFvqkIfnUMXLoa6gcjA2Uil_kRQKiSBc6PMk,332
14
+ llmrix/skill/services/manager.py,sha256=VucU_M781BYINSn5n5r_47RTJCZJXZTA-tSzFHcAUGc,3592
15
+ llmrix/skill/services/publisher.py,sha256=Sx43RaoOS2orx_sVXRM4QmnEZixZnAyPdTmctB_sy1k,5357
16
+ llmrix/skill/services/syncer.py,sha256=fJvyiqRCRJZsyhwnSQjKDPDlAa3bPk7ECHgkhzw_DNo,4765
17
+ llmrix/skill/storage/__init__.py,sha256=wsx-Tj003rFOQMSKUjeXz-brGYNyCe0k8CJqYkT8_Qc,256
18
+ llmrix/skill/storage/base.py,sha256=smIIDR6hQtpfpD0GWAWjjC_SVutuNO7uTShkjGU4VAo,1177
19
+ llmrix/skill/storage/mysql.py,sha256=V1Xc9HgfSzAv-XKOr8LygMZN9MHqj4Cf_O9TVIOIt-s,4054
20
+ llmrix/skill/storage/sqlalchemy_store.py,sha256=9MmZu4KfuZch-U8j9-fov--1pcoclhFFK--IpH0QYp8,5796
21
+ llmrix_skill-0.2.0.dist-info/METADATA,sha256=8B-yX0Iw-qr2wUMHVI_M5EOm9Ay3K61euVsnVX_d3Gc,5289
22
+ llmrix_skill-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
+ llmrix_skill-0.2.0.dist-info/top_level.txt,sha256=TAjGcnZd2kd6XpS1jGciyU_vDj6rQyhDqHmOfJVW1fw,7
24
+ llmrix_skill-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ llmrix