dynamic-skill-loader 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aimmetal-tech
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,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: dynamic-skill-loader
3
+ Version: 0.1.0
4
+ Summary: Dynamic skill loader with Jev ranking
5
+ Author: aimmetal-tech
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Dist: mcp[cli]>=2.2.0
9
+ Requires-Dist: pydantic>=2.13.5
10
+ Requires-Dist: pydantic-settings>=2.15.0
11
+ Requires-Dist: python-frontmatter>=1.3.0
12
+ Requires-Dist: typesafe-sdk>=0.7.0
13
+ Requires-Python: >=3.12, <3.13
14
+ Description-Content-Type: text/markdown
15
+
16
+ <div align="center">
17
+
18
+ # Dynamic Skill Loader
19
+
20
+ [![Python 3.12](https://img.shields.io/badge/Python-3.12-3776AB?logo=python&logoColor=white)](https://www.python.org/)
21
+ [![uv](https://img.shields.io/badge/managed%20with-uv-DE5FE9?logo=uv&logoColor=white)](https://docs.astral.sh/uv/)
22
+ [![MCP](https://img.shields.io/badge/MCP-server-5B5BD6)](https://modelcontextprotocol.io/)
23
+ [![TypeSafe](https://img.shields.io/badge/ranking-TypeSafe-111827)](https://typesafe.ai/)
24
+ [![Ruff](https://img.shields.io/badge/linting-Ruff-D7FF64?logo=ruff&logoColor=111827)](https://docs.astral.sh/ruff/)
25
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
26
+
27
+ An MCP server that discovers local skills and ranks them against a user's request with TypeSafe.
28
+
29
+ [English](README.md) | [简体中文](README.zh-CN.md)
30
+
31
+ </div>
32
+
33
+ ## What it does
34
+
35
+ Dynamic Skill Loader is an MCP server that exposes a `retrieve_skills` tool. Given keywords and an optional original request, it:
36
+
37
+ 1. Detects the connected MCP client's Harness.
38
+ 2. Selects a skill root, honoring `--skills-dir` before Harness defaults.
39
+ 3. Reads each skill's YAML front matter.
40
+ 4. Uses TypeSafe to estimate relevance.
41
+ 5. Returns `SkillScoreModel` values sorted by descending score.
42
+
43
+ Each result includes the skill name, relevance score, and an absolute path to its `SKILL.md` file. A client can use that path to read the full skill instructions.
44
+
45
+ The default skill roots are `~/.codex/skills` for Codex, `~/.claude/skills` for Claude Code, and `~/.agents/skills` for other Harness values. Use `--skills-dir` to select any compatible skill directory explicitly.
46
+
47
+ ## Requirements
48
+
49
+ - Python 3.12
50
+ - [uv](https://docs.astral.sh/uv/)
51
+ - A TypeSafe API key for live ranking
52
+
53
+ ## Installation
54
+
55
+ For local development:
56
+
57
+ ```powershell
58
+ uv sync
59
+ ```
60
+
61
+ For end users, `uvx` can install and run the published package on demand. Pass `TYPESAFE_API_KEY` and optional `--skills-dir` after the package entry point.
62
+
63
+ ## Run with uvx
64
+
65
+ An MCP client can launch the published package with:
66
+
67
+ ```json
68
+ {
69
+ "command": "uvx",
70
+ "args": [
71
+ "dynamic-skill-loader",
72
+ "--TYPESAFE_API_KEY",
73
+ "your-typesafe-api-key",
74
+ "--skills-dir",
75
+ "~/.agents/skills"
76
+ ]
77
+ }
78
+ ```
79
+
80
+ The equivalent command line is:
81
+
82
+ ```powershell
83
+ uvx dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ~/.agents/skills
84
+ ```
85
+
86
+ To run a local checkout instead:
87
+
88
+ ```powershell
89
+ uv run dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ./skills
90
+ ```
91
+
92
+ `--skills-dir` has the highest priority. `TYPESAFE_BASE_URL` remains an environment variable for non-default TypeSafe endpoints. Avoid committing API keys to configuration files that are shared publicly.
93
+
94
+ The server exposes:
95
+
96
+ ```text
97
+ retrieve_skills(keyword: list[str], original_request: str | null = null)
98
+ ```
99
+
100
+ Example input:
101
+
102
+ ```json
103
+ {
104
+ "keyword": ["web search", "recent news"],
105
+ "original_request": "Find a skill for searching today's news."
106
+ }
107
+ ```
108
+
109
+ ## Add a skill
110
+
111
+ Create a direct child directory under the selected skill root with a `SKILL.md` file. The YAML front matter must include `name` and `description`:
112
+
113
+ ```text
114
+ <skills-root>/my-skill/SKILL.md
115
+ ```
116
+
117
+ ```markdown
118
+ ---
119
+ name: my-skill
120
+ description: A concise description of when this skill should be used.
121
+ ---
122
+
123
+ # My Skill
124
+
125
+ Operational instructions for the skill go here.
126
+ ```
127
+
128
+ The loader does not search nested skill directories. Keep detailed references beside the skill and link to them from `SKILL.md`. For local development, `--skills-dir ./skills` can be used explicitly.
129
+
130
+ ## Project structure
131
+
132
+ ```text
133
+ dynamic-skill-loader/
134
+ ├── src/dynamic_skill_loader/
135
+ │ ├── cli.py # MCP server, arguments, and retrieve_skills
136
+ │ ├── jev_rank.py # TypeSafe ranking workflow
137
+ │ ├── model.py # SkillScoreModel
138
+ │ ├── util_detect_harness.py # MCP client Harness detection
139
+ │ └── util_read_skill.py # Skill roots, discovery, and metadata parsing
140
+ ├── tests/ # Focused unit tests
141
+ ├── skills/ # Optional local development skill root
142
+ ├── pyproject.toml # Package metadata and dynamic-skill-loader entry point
143
+ ├── uv.lock # Locked dependencies
144
+ ├── LICENSE # MIT license
145
+ ├── README.md
146
+ └── README.zh-CN.md
147
+ ```
148
+
149
+ The published wheel contains the Python server. Skills are loaded from the selected external root at runtime rather than bundled into the wheel.
150
+
151
+ ## Development
152
+
153
+ Run the test suite and checks through `uv`:
154
+
155
+ ```powershell
156
+ uv run pytest
157
+ uv run ruff check .
158
+ uv run ruff format --check .
159
+ ```
160
+
161
+ Tests stub TypeSafe and skill discovery, so they do not require credentials or network access. See [AGENTS.md](AGENTS.md) for architecture notes and project-specific conventions.
162
+
163
+ ## License
164
+
165
+ MIT License. See [LICENSE](LICENSE) for the full text.
@@ -0,0 +1,150 @@
1
+ <div align="center">
2
+
3
+ # Dynamic Skill Loader
4
+
5
+ [![Python 3.12](https://img.shields.io/badge/Python-3.12-3776AB?logo=python&logoColor=white)](https://www.python.org/)
6
+ [![uv](https://img.shields.io/badge/managed%20with-uv-DE5FE9?logo=uv&logoColor=white)](https://docs.astral.sh/uv/)
7
+ [![MCP](https://img.shields.io/badge/MCP-server-5B5BD6)](https://modelcontextprotocol.io/)
8
+ [![TypeSafe](https://img.shields.io/badge/ranking-TypeSafe-111827)](https://typesafe.ai/)
9
+ [![Ruff](https://img.shields.io/badge/linting-Ruff-D7FF64?logo=ruff&logoColor=111827)](https://docs.astral.sh/ruff/)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
+
12
+ An MCP server that discovers local skills and ranks them against a user's request with TypeSafe.
13
+
14
+ [English](README.md) | [简体中文](README.zh-CN.md)
15
+
16
+ </div>
17
+
18
+ ## What it does
19
+
20
+ Dynamic Skill Loader is an MCP server that exposes a `retrieve_skills` tool. Given keywords and an optional original request, it:
21
+
22
+ 1. Detects the connected MCP client's Harness.
23
+ 2. Selects a skill root, honoring `--skills-dir` before Harness defaults.
24
+ 3. Reads each skill's YAML front matter.
25
+ 4. Uses TypeSafe to estimate relevance.
26
+ 5. Returns `SkillScoreModel` values sorted by descending score.
27
+
28
+ Each result includes the skill name, relevance score, and an absolute path to its `SKILL.md` file. A client can use that path to read the full skill instructions.
29
+
30
+ The default skill roots are `~/.codex/skills` for Codex, `~/.claude/skills` for Claude Code, and `~/.agents/skills` for other Harness values. Use `--skills-dir` to select any compatible skill directory explicitly.
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.12
35
+ - [uv](https://docs.astral.sh/uv/)
36
+ - A TypeSafe API key for live ranking
37
+
38
+ ## Installation
39
+
40
+ For local development:
41
+
42
+ ```powershell
43
+ uv sync
44
+ ```
45
+
46
+ For end users, `uvx` can install and run the published package on demand. Pass `TYPESAFE_API_KEY` and optional `--skills-dir` after the package entry point.
47
+
48
+ ## Run with uvx
49
+
50
+ An MCP client can launch the published package with:
51
+
52
+ ```json
53
+ {
54
+ "command": "uvx",
55
+ "args": [
56
+ "dynamic-skill-loader",
57
+ "--TYPESAFE_API_KEY",
58
+ "your-typesafe-api-key",
59
+ "--skills-dir",
60
+ "~/.agents/skills"
61
+ ]
62
+ }
63
+ ```
64
+
65
+ The equivalent command line is:
66
+
67
+ ```powershell
68
+ uvx dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ~/.agents/skills
69
+ ```
70
+
71
+ To run a local checkout instead:
72
+
73
+ ```powershell
74
+ uv run dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ./skills
75
+ ```
76
+
77
+ `--skills-dir` has the highest priority. `TYPESAFE_BASE_URL` remains an environment variable for non-default TypeSafe endpoints. Avoid committing API keys to configuration files that are shared publicly.
78
+
79
+ The server exposes:
80
+
81
+ ```text
82
+ retrieve_skills(keyword: list[str], original_request: str | null = null)
83
+ ```
84
+
85
+ Example input:
86
+
87
+ ```json
88
+ {
89
+ "keyword": ["web search", "recent news"],
90
+ "original_request": "Find a skill for searching today's news."
91
+ }
92
+ ```
93
+
94
+ ## Add a skill
95
+
96
+ Create a direct child directory under the selected skill root with a `SKILL.md` file. The YAML front matter must include `name` and `description`:
97
+
98
+ ```text
99
+ <skills-root>/my-skill/SKILL.md
100
+ ```
101
+
102
+ ```markdown
103
+ ---
104
+ name: my-skill
105
+ description: A concise description of when this skill should be used.
106
+ ---
107
+
108
+ # My Skill
109
+
110
+ Operational instructions for the skill go here.
111
+ ```
112
+
113
+ The loader does not search nested skill directories. Keep detailed references beside the skill and link to them from `SKILL.md`. For local development, `--skills-dir ./skills` can be used explicitly.
114
+
115
+ ## Project structure
116
+
117
+ ```text
118
+ dynamic-skill-loader/
119
+ ├── src/dynamic_skill_loader/
120
+ │ ├── cli.py # MCP server, arguments, and retrieve_skills
121
+ │ ├── jev_rank.py # TypeSafe ranking workflow
122
+ │ ├── model.py # SkillScoreModel
123
+ │ ├── util_detect_harness.py # MCP client Harness detection
124
+ │ └── util_read_skill.py # Skill roots, discovery, and metadata parsing
125
+ ├── tests/ # Focused unit tests
126
+ ├── skills/ # Optional local development skill root
127
+ ├── pyproject.toml # Package metadata and dynamic-skill-loader entry point
128
+ ├── uv.lock # Locked dependencies
129
+ ├── LICENSE # MIT license
130
+ ├── README.md
131
+ └── README.zh-CN.md
132
+ ```
133
+
134
+ The published wheel contains the Python server. Skills are loaded from the selected external root at runtime rather than bundled into the wheel.
135
+
136
+ ## Development
137
+
138
+ Run the test suite and checks through `uv`:
139
+
140
+ ```powershell
141
+ uv run pytest
142
+ uv run ruff check .
143
+ uv run ruff format --check .
144
+ ```
145
+
146
+ Tests stub TypeSafe and skill discovery, so they do not require credentials or network access. See [AGENTS.md](AGENTS.md) for architecture notes and project-specific conventions.
147
+
148
+ ## License
149
+
150
+ MIT License. See [LICENSE](LICENSE) for the full text.
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "dynamic-skill-loader"
3
+ version = "0.1.0"
4
+ description = "Dynamic skill loader with Jev ranking"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12,<3.13"
9
+ dependencies = [
10
+ "mcp[cli]>=2.2.0",
11
+ "pydantic>=2.13.5",
12
+ "pydantic-settings>=2.15.0",
13
+ "python-frontmatter>=1.3.0",
14
+ "typesafe-sdk>=0.7.0",
15
+ ]
16
+
17
+ [[project.authors]]
18
+ name = "aimmetal-tech"
19
+
20
+ [project.scripts]
21
+ dynamic-skill-loader = "dynamic_skill_loader:main"
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.12.15,<0.13.0"]
25
+ build-backend = "uv_build"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest>=9.1.1",
30
+ "ruff>=0.11",
31
+ ]
32
+
33
+ [tool.ruff]
34
+ target-version = "py312"
35
+ line-length = 88
36
+ src = [
37
+ "src",
38
+ "tests",
39
+ ]
40
+
41
+ [tool.ruff.format]
42
+ quote-style = "double"
43
+ indent-style = "space"
44
+ line-ending = "auto"
45
+
46
+ [tool.ruff.lint]
47
+ select = [
48
+ "E4",
49
+ "E7",
50
+ "E9",
51
+ "F",
52
+ "I",
53
+ ]
54
+
55
+ [tool.ruff.lint.isort]
56
+ known-first-party = ["dynamic_skill_loader"]
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "dynamic-skill-loader"
3
+ version = "0.1.0"
4
+ description = "Dynamic skill loader with Jev ranking"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "aimmetal-tech" }
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.12,<3.13"
12
+ dependencies = [
13
+ "mcp[cli]>=2.2.0",
14
+ "pydantic>=2.13.5",
15
+ "pydantic-settings>=2.15.0",
16
+ "python-frontmatter>=1.3.0",
17
+ "typesafe-sdk>=0.7.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ dynamic-skill-loader = "dynamic_skill_loader:main"
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.12.15,<0.13.0"]
25
+ build-backend = "uv_build"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest>=9.1.1",
30
+ "ruff>=0.11",
31
+ ]
32
+
33
+
34
+ [tool.ruff]
35
+ target-version = "py312"
36
+ line-length = 88
37
+ src = ["src", "tests"]
38
+
39
+ [tool.ruff.format]
40
+ quote-style = "double"
41
+ indent-style = "space"
42
+ line-ending = "auto"
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E4", "E7", "E9", "F", "I"]
46
+
47
+ [tool.ruff.lint.isort]
48
+ known-first-party = ["dynamic_skill_loader"]
@@ -0,0 +1,3 @@
1
+ from dynamic_skill_loader.cli import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,84 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ from mcp.server import MCPServer
9
+ from mcp.server.mcpserver import Context
10
+ from pydantic import Field
11
+
12
+ from dynamic_skill_loader.jev_rank import rank_by_state
13
+ from dynamic_skill_loader.model import SkillScoreModel
14
+ from dynamic_skill_loader.util_detect_harness import detect_harness
15
+
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
19
+ stream=sys.stderr,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ mcp = MCPServer("my-mcp")
25
+ SKILLS_DIR: Path | None = None
26
+
27
+ STATE = """<KEYWORD>
28
+ {keyword}
29
+ </KEYWORD>
30
+
31
+ <ORIGINAL_REQUEST>
32
+ {original_request}
33
+ </ORIGINAL_REQUEST>
34
+ """
35
+
36
+
37
+ def build_state(keyword, original_request) -> str:
38
+ return STATE.format(keyword=", ".join(keyword), original_request=original_request)
39
+
40
+
41
+ @mcp.tool()
42
+ def retrieve_skills(
43
+ ctx: Context,
44
+ keyword: Annotated[
45
+ list[str],
46
+ Field(
47
+ description="The keyword of user's question or request, using to search relevant skills."
48
+ ),
49
+ ],
50
+ original_request: Annotated[
51
+ str | None, Field(description="The original request or question from user.")
52
+ ] = None,
53
+ ) -> dict[str, SkillScoreModel]:
54
+ """Retrieve skills by keyword and original request"""
55
+ harness = detect_harness(ctx)
56
+ logger.info("Detected agent harness: %s", harness)
57
+ state = build_state(keyword, original_request)
58
+ answer = rank_by_state(state, harness, skills_dir=SKILLS_DIR)
59
+ return answer
60
+
61
+
62
+ # @mcp.tool
63
+ # def open_skills(
64
+ # path: Annotated[str, Field(description="Absolute root path of the skill")],
65
+ # ):
66
+ # """ "Open skill by name"""
67
+ # skill_root_abs_path = Path(path)
68
+
69
+
70
+ def main():
71
+ global SKILLS_DIR
72
+
73
+ parser = argparse.ArgumentParser(add_help=False)
74
+ parser.add_argument("--skills-dir", type=Path)
75
+ parser.add_argument("--TYPESAFE_API_KEY")
76
+ args, _ = parser.parse_known_args()
77
+ SKILLS_DIR = args.skills_dir
78
+
79
+ if args.TYPESAFE_API_KEY is not None:
80
+ os.environ["TYPESAFE_API_KEY"] = args.TYPESAFE_API_KEY
81
+
82
+ if SKILLS_DIR is not None:
83
+ logger.info("Using skills directory from --skills-dir: %s", SKILLS_DIR)
84
+ mcp.run()
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+
3
+ from typesafe_sdk import Choice, TypeSafeClient
4
+
5
+ from dynamic_skill_loader.model import SkillScoreModel
6
+ from dynamic_skill_loader.util_detect_harness import AgentHarness
7
+ from dynamic_skill_loader.util_read_skill import (
8
+ find_skill_meta_files,
9
+ get_skill_root,
10
+ read_skill_metadata,
11
+ )
12
+
13
+
14
+ def rank_by_state(
15
+ state: str,
16
+ harness: AgentHarness = AgentHarness.UNKNOWN,
17
+ skills_dir: Path | None = None,
18
+ ):
19
+ client = TypeSafeClient()
20
+
21
+ skills_path = find_skill_meta_files(
22
+ root=get_skill_root(harness, skills_dir=skills_dir)
23
+ )
24
+
25
+ skills_criteria = {}
26
+
27
+ dict_model: dict[str, SkillScoreModel] = {}
28
+
29
+ for skill_path in skills_path:
30
+ meta_json_str = read_skill_metadata(skill_path)
31
+ skill_name = meta_json_str["name"]
32
+ skills_criteria[skill_name] = meta_json_str
33
+
34
+ dict_model[skill_name] = SkillScoreModel(
35
+ name=skill_name, score=0.0, abs_path=skill_path
36
+ )
37
+
38
+ response = client.system_one(
39
+ state=state,
40
+ questions={
41
+ "skill_relevant": Choice(
42
+ instructions="Is this skill relevant to the user's question/request?",
43
+ criteria=skills_criteria,
44
+ )
45
+ },
46
+ )
47
+
48
+ answer = response.choices["skill_relevant"]
49
+ ranking = sorted(answer.probabilities.items(), key=lambda kv: kv[1], reverse=True)
50
+ for name, score in ranking:
51
+ model = dict_model[name]
52
+ model.score = score
53
+
54
+ return dict(
55
+ sorted(dict_model.items(), key=lambda item: item[1].score, reverse=True)
56
+ )
57
+
58
+
59
+ if __name__ == "__main__":
60
+ rank_by_state("搜索今日新闻")
@@ -0,0 +1,11 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class SkillScoreModel(BaseModel):
7
+ name: str = Field(description="Skill's name.")
8
+ score: float = Field(
9
+ description="The score of the match between the skill and user's request. The higher the score is, the better the match."
10
+ )
11
+ abs_path: Path = Field(description="The absolute path of the skill")
@@ -0,0 +1,51 @@
1
+ from enum import StrEnum
2
+
3
+ from mcp.server.mcpserver import Context
4
+
5
+
6
+ class AgentHarness(StrEnum):
7
+ CODEX = "codex"
8
+ CLAUDE_CODE = "claude-code"
9
+ PI = "pi"
10
+ OMP = "omp"
11
+ GITHUB_COPILOT = "github-copilot"
12
+ UNKNOWN = "unknown"
13
+
14
+
15
+ HARNESS_MAP = {
16
+ "codex-mcp-client": AgentHarness.CODEX,
17
+ "codex": AgentHarness.CODEX,
18
+ "codex-cli": AgentHarness.CODEX,
19
+ "openai-codex": AgentHarness.CODEX,
20
+ "claude-code": AgentHarness.CLAUDE_CODE,
21
+ "claude-ai": AgentHarness.CLAUDE_CODE,
22
+ "claude": AgentHarness.CLAUDE_CODE,
23
+ "claude-code-cli": AgentHarness.CLAUDE_CODE,
24
+ "omp-coding-agent": AgentHarness.OMP,
25
+ "oh-my-pi": AgentHarness.OMP,
26
+ "pi": AgentHarness.PI,
27
+ "pi-cli": AgentHarness.PI,
28
+ "pi-coding-agent": AgentHarness.PI,
29
+ "github-copilot-developer": AgentHarness.GITHUB_COPILOT,
30
+ "copilot-cli": AgentHarness.GITHUB_COPILOT,
31
+ "github-copilot": AgentHarness.GITHUB_COPILOT,
32
+ "github-copilot-vscode": AgentHarness.GITHUB_COPILOT,
33
+ "visual-studio-code": AgentHarness.GITHUB_COPILOT,
34
+ "vscode": AgentHarness.GITHUB_COPILOT,
35
+ }
36
+
37
+
38
+ def detect_harness(ctx: Context) -> AgentHarness:
39
+ client_info = getattr(
40
+ getattr(ctx.session, "client_params", None),
41
+ "client_info",
42
+ None,
43
+ )
44
+
45
+ name = getattr(client_info, "name", "")
46
+ normalized_name = "-".join(name.strip().casefold().replace("_", "-").split())
47
+
48
+ return HARNESS_MAP.get(
49
+ normalized_name,
50
+ AgentHarness.UNKNOWN,
51
+ )
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+
3
+ import frontmatter
4
+
5
+ from dynamic_skill_loader.util_detect_harness import AgentHarness
6
+
7
+
8
+ def get_skill_root(
9
+ harness: AgentHarness,
10
+ home: Path | None = None,
11
+ skills_dir: Path | None = None,
12
+ ) -> Path:
13
+ if skills_dir is not None:
14
+ return skills_dir.expanduser()
15
+
16
+ home = Path.home() if home is None else home
17
+
18
+ if harness is AgentHarness.CODEX:
19
+ return home / ".codex" / "skills"
20
+ if harness is AgentHarness.CLAUDE_CODE:
21
+ return home / ".claude" / "skills"
22
+ return home / ".agents" / "skills"
23
+
24
+
25
+ def find_skill_meta_files(root: Path) -> list[Path]:
26
+ if not root.is_dir():
27
+ raise NotADirectoryError(f"不是有效目录:{root}")
28
+ root = root.resolve()
29
+ return sorted(root.glob("*/SKILL.md"), key=lambda p: p.parent.name)
30
+
31
+
32
+ def read_skill_metadata(path: Path):
33
+ post = frontmatter.load(path)
34
+ return post.metadata
35
+
36
+
37
+ if __name__ == "__main__":
38
+ skills_path = find_skill_meta_files(root=Path("skills"))
39
+ for skill_path in skills_path:
40
+ print(skill_path)
41
+ meta = read_skill_metadata(Path("skills/tavily-search/SKILL.md"))
42
+ print(meta["name"])