skill-automation-package 0.2.0
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.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/assets/.claude/skills/project-skill-router/SKILL.md +32 -0
- package/assets/.claude/skills/project-skill-router/skill.json +50 -0
- package/assets/.claude/tests/test_skill_agent.py +508 -0
- package/assets/.claude/tools/skill_agent.py +2501 -0
- package/bin/skill-automation-package.js +402 -0
- package/package.json +42 -0
- package/scripts/install.py +208 -0
- package/scripts/package_layout.py +108 -0
- package/templates/agents_block.md +20 -0
- package/templates/claude_block.md +11 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
ASSETS_ROOT = PACKAGE_ROOT / "assets"
|
|
11
|
+
TEMPLATES_ROOT = PACKAGE_ROOT / "templates"
|
|
12
|
+
PACKAGE_MANIFEST = PACKAGE_ROOT / "package.json"
|
|
13
|
+
EXECUTABLE_FILE_MODE = 0o755
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class PackageLayout:
|
|
18
|
+
name: str
|
|
19
|
+
version: str
|
|
20
|
+
managed_assets: tuple[Path, ...]
|
|
21
|
+
optional_assets: tuple[Path, ...]
|
|
22
|
+
executable_assets: frozenset[Path]
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def all_assets(self) -> list[Path]:
|
|
26
|
+
return [*self.managed_assets, *self.optional_assets]
|
|
27
|
+
|
|
28
|
+
def selected_assets(self, *, include_optional: bool) -> list[Path]:
|
|
29
|
+
if include_optional:
|
|
30
|
+
return self.all_assets
|
|
31
|
+
return list(self.managed_assets)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_package_layout(manifest_path: Path = PACKAGE_MANIFEST) -> PackageLayout:
|
|
35
|
+
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
36
|
+
managed_assets = normalize_manifest_paths(
|
|
37
|
+
payload.get("managed_assets") or payload.get("assets", [])
|
|
38
|
+
)
|
|
39
|
+
optional_assets = normalize_manifest_paths(payload.get("optional_assets", []))
|
|
40
|
+
executable_assets = frozenset(normalize_manifest_paths(payload.get("executable_assets", [])))
|
|
41
|
+
return PackageLayout(
|
|
42
|
+
name=str(payload["name"]),
|
|
43
|
+
version=str(payload["version"]),
|
|
44
|
+
managed_assets=managed_assets,
|
|
45
|
+
optional_assets=optional_assets,
|
|
46
|
+
executable_assets=executable_assets,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def normalize_manifest_paths(values: object) -> tuple[Path, ...]:
|
|
51
|
+
if not isinstance(values, list):
|
|
52
|
+
return ()
|
|
53
|
+
normalized: list[Path] = []
|
|
54
|
+
seen: set[Path] = set()
|
|
55
|
+
for value in values:
|
|
56
|
+
if not isinstance(value, str):
|
|
57
|
+
continue
|
|
58
|
+
path = Path(value)
|
|
59
|
+
if path in seen:
|
|
60
|
+
continue
|
|
61
|
+
normalized.append(path)
|
|
62
|
+
seen.add(path)
|
|
63
|
+
return tuple(normalized)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def iter_asset_files(source_root: Path, asset_paths: list[Path]) -> list[tuple[Path, Path]]:
|
|
67
|
+
resolved_files: list[tuple[Path, Path]] = []
|
|
68
|
+
seen: set[Path] = set()
|
|
69
|
+
for relative_path in asset_paths:
|
|
70
|
+
source_path = source_root / relative_path
|
|
71
|
+
if not source_path.exists():
|
|
72
|
+
raise FileNotFoundError(f"Missing asset: {source_path}")
|
|
73
|
+
if source_path.is_dir():
|
|
74
|
+
for candidate in sorted(source_path.rglob("*")):
|
|
75
|
+
if not candidate.is_file():
|
|
76
|
+
continue
|
|
77
|
+
asset_file = candidate.relative_to(source_root)
|
|
78
|
+
if asset_file in seen:
|
|
79
|
+
continue
|
|
80
|
+
resolved_files.append((candidate, asset_file))
|
|
81
|
+
seen.add(asset_file)
|
|
82
|
+
continue
|
|
83
|
+
if relative_path in seen:
|
|
84
|
+
continue
|
|
85
|
+
resolved_files.append((source_path, relative_path))
|
|
86
|
+
seen.add(relative_path)
|
|
87
|
+
return resolved_files
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def copy_assets(
|
|
91
|
+
*,
|
|
92
|
+
source_root: Path,
|
|
93
|
+
destination_root: Path,
|
|
94
|
+
asset_paths: list[Path],
|
|
95
|
+
executable_assets: frozenset[Path],
|
|
96
|
+
dry_run: bool = False,
|
|
97
|
+
) -> list[Path]:
|
|
98
|
+
copied: list[Path] = []
|
|
99
|
+
for source_path, relative_path in iter_asset_files(source_root, asset_paths):
|
|
100
|
+
destination_path = destination_root / relative_path
|
|
101
|
+
copied.append(destination_path)
|
|
102
|
+
if dry_run:
|
|
103
|
+
continue
|
|
104
|
+
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
shutil.copy2(source_path, destination_path)
|
|
106
|
+
if relative_path in executable_assets:
|
|
107
|
+
destination_path.chmod(EXECUTABLE_FILE_MODE)
|
|
108
|
+
return copied
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- SKILL-AUTOMATION:AGENTS:START -->
|
|
2
|
+
## Repo Local Skill Automation
|
|
3
|
+
|
|
4
|
+
For any non-trivial task in this repository, start with:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
python3 .claude/tools/skill_agent.py auto "<task>" --json
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- If the action is `reuse`, open the matched skill under `.claude/skills/<skill-name>/SKILL.md` and follow it.
|
|
11
|
+
- If the action is `created`, use the generated skill immediately. The registry is already refreshed.
|
|
12
|
+
- If a step inside the chosen workflow becomes its own repeatable, non-trivial subtask, rerun `python3 .claude/tools/skill_agent.py auto "<sub-task>" --json` for that step, then resume the parent workflow.
|
|
13
|
+
- If you want a preview before writing files, run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python3 .claude/tools/skill_agent.py auto "<task>" --dry-run --json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Use `suggest`, `search`, `bootstrap`, `create`, and `refresh` only when the default `auto` flow is not enough.
|
|
20
|
+
<!-- SKILL-AUTOMATION:AGENTS:END -->
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- SKILL-AUTOMATION:CLAUDE:START -->
|
|
2
|
+
## Local Skill Automation
|
|
3
|
+
|
|
4
|
+
- Store repo-local skills under `.claude/skills/<skill-name>/`.
|
|
5
|
+
- For any non-trivial task, start with `python3 .claude/tools/skill_agent.py auto "<task>" --json`.
|
|
6
|
+
- If `auto` returns `reuse`, open the matched skill and follow it.
|
|
7
|
+
- If `auto` returns `created`, use the generated skill immediately. The registry is updated automatically.
|
|
8
|
+
- If a step inside the current workflow becomes its own repeatable, non-trivial subtask, rerun `python3 .claude/tools/skill_agent.py auto "<sub-task>" --json` for that step before expanding the parent flow manually.
|
|
9
|
+
- To preview a generated skill before writing files, use `python3 .claude/tools/skill_agent.py auto "<task>" --dry-run --json`.
|
|
10
|
+
- Use `suggest`, `search`, `bootstrap`, `create`, and `refresh` only when the default `auto` flow is not enough.
|
|
11
|
+
<!-- SKILL-AUTOMATION:CLAUDE:END -->
|