mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-02 07:22:32 +00:00
feat(skills): add Agent Skill Stack (#2350)
Co-authored-by: neilchen2000-pixel <283394672+neilchen2000-pixel@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only inventory and overlap/risk indicator scan for agent skills."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
SKIP_DIRS = {
|
||||
".git",
|
||||
".archive",
|
||||
".curator_backups",
|
||||
".hub",
|
||||
"__pycache__",
|
||||
"node_modules",
|
||||
}
|
||||
SCRIPT_SUFFIXES = {".py", ".sh", ".js", ".ts", ".mjs", ".cjs", ".ps1", ".rb", ".go"}
|
||||
STOPWORDS = {
|
||||
"about", "agent", "agents", "also", "and", "any", "are", "can", "for", "from",
|
||||
"help", "into", "its", "other", "skill", "skills", "that", "the", "their", "this",
|
||||
"through", "tool", "tools", "use", "user", "users", "using", "when", "with", "workflow",
|
||||
"一个", "一款", "一些", "什么", "可以", "帮我", "技能", "我想", "有没有", "这个", "这件",
|
||||
}
|
||||
RISK_PATTERNS = {
|
||||
"destructive-command": re.compile(r"\brm\s+-[^\n]*r[^\n]*f|git\s+reset\s+--hard|shutil\.rmtree", re.I),
|
||||
"credential-or-secret-access": re.compile(
|
||||
r"\.ssh\b|\.aws\b|keychain|credential|cookie|secret|api[_-]?key|\.env\b|os\.environ", re.I
|
||||
),
|
||||
"network-or-download": re.compile(
|
||||
r"\bcurl\b|\bwget\b|requests\.|httpx\.|urllib\.|fetch\s*\(|https?://", re.I
|
||||
),
|
||||
"dynamic-or-obfuscated-execution": re.compile(
|
||||
r"base64[^\n]{0,80}(decode|-d)|\beval\s*\(|\bexec\s*\(|child_process|subprocess\.", re.I
|
||||
),
|
||||
"persistence-or-system-service": re.compile(r"\bcrontab\b|\blaunchctl\b|systemctl\s+enable|launchagents", re.I),
|
||||
"privilege-or-broad-permission": re.compile(r"\bsudo\b|chmod\s+777|chown\s+-R", re.I),
|
||||
"external-mutation-language": re.compile(
|
||||
r"\b(publish|send|upload|delete|remove|purchase|comment|post)\b|发布|发送|上传|删除|购买|评论", re.I
|
||||
),
|
||||
"possible-hardcoded-token": re.compile(r"\b(?:sk|ghp|github_pat)_[A-Za-z0-9_-]{12,}\b|\bAKIA[A-Z0-9]{12,}\b"),
|
||||
}
|
||||
|
||||
|
||||
def parse_frontmatter(text: str) -> tuple[dict[str, str], list[str]]:
|
||||
issues: list[str] = []
|
||||
lines = text.splitlines()
|
||||
if not lines or lines[0].strip() != "---":
|
||||
return {}, ["missing opening frontmatter delimiter"]
|
||||
try:
|
||||
end = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
|
||||
except StopIteration:
|
||||
return {}, ["missing closing frontmatter delimiter"]
|
||||
|
||||
data: dict[str, str] = {}
|
||||
i = 1
|
||||
while i < end:
|
||||
match = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", lines[i])
|
||||
if not match:
|
||||
i += 1
|
||||
continue
|
||||
key, raw = match.group(1), match.group(2).strip()
|
||||
if raw in {">", "|"}:
|
||||
mode = raw
|
||||
block: list[str] = []
|
||||
i += 1
|
||||
while i < end and (not lines[i].strip() or lines[i][:1].isspace()):
|
||||
block.append(lines[i].strip())
|
||||
i += 1
|
||||
data[key] = (" " if mode == ">" else "\n").join(part for part in block if part)
|
||||
continue
|
||||
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in {'"', "'"}:
|
||||
raw = raw[1:-1]
|
||||
data[key] = raw
|
||||
i += 1
|
||||
|
||||
if not data.get("name"):
|
||||
issues.append("missing name")
|
||||
if not data.get("description"):
|
||||
issues.append("missing description")
|
||||
return data, issues
|
||||
|
||||
|
||||
def iter_skill_files(root: Path) -> Iterable[Path]:
|
||||
for current, dirs, files in os.walk(root, followlinks=False):
|
||||
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
|
||||
if "SKILL.md" in files:
|
||||
yield Path(current) / "SKILL.md"
|
||||
|
||||
|
||||
def tokenize(text: str) -> set[str]:
|
||||
tokens = {
|
||||
token for token in re.findall(r"[a-z][a-z0-9-]{2,}", text.lower())
|
||||
if token not in STOPWORDS
|
||||
}
|
||||
for run in re.findall(r"[\u3400-\u9fff]{2,}", text):
|
||||
if len(run) <= 8 and run not in STOPWORDS:
|
||||
tokens.add(run)
|
||||
tokens.update(run[i:i + 2] for i in range(len(run) - 1) if run[i:i + 2] not in STOPWORDS)
|
||||
return set(sorted(tokens)[:120])
|
||||
|
||||
|
||||
def scan_indicators(skill_dir: Path) -> list[dict[str, object]]:
|
||||
findings: dict[tuple[str, str], int] = {}
|
||||
candidates = [skill_dir / "SKILL.md"]
|
||||
for current, dirs, files in os.walk(skill_dir, followlinks=False):
|
||||
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
|
||||
for filename in files:
|
||||
path = Path(current) / filename
|
||||
if path == skill_dir / "SKILL.md":
|
||||
continue
|
||||
if path.suffix.lower() in SCRIPT_SUFFIXES or filename in {"package.json", "pyproject.toml"}:
|
||||
candidates.append(path)
|
||||
|
||||
for path in sorted(set(candidates))[:250]:
|
||||
try:
|
||||
if path.is_symlink() or path.stat().st_size > 1_000_000:
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
relative = str(path.relative_to(skill_dir))
|
||||
for label, pattern in RISK_PATTERNS.items():
|
||||
count = len(pattern.findall(text))
|
||||
if count:
|
||||
findings[(label, relative)] = count
|
||||
|
||||
return [
|
||||
{"indicator": label, "file": filename, "matches": count}
|
||||
for (label, filename), count in sorted(findings.items())
|
||||
]
|
||||
|
||||
|
||||
def skill_record(skill_file: Path, root: Path) -> dict[str, object]:
|
||||
try:
|
||||
text = skill_file.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as exc:
|
||||
return {"path": str(skill_file.parent), "root": str(root), "issues": [f"read error: {exc}"]}
|
||||
|
||||
data, issues = parse_frontmatter(text[:300_000])
|
||||
name = data.get("name", "")
|
||||
description = data.get("description", "")
|
||||
if name and skill_file.parent.name != name:
|
||||
issues.append(f"directory name '{skill_file.parent.name}' differs from skill name '{name}'")
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"path": str(skill_file.parent),
|
||||
"root": str(root),
|
||||
"trigger_tokens": sorted(tokenize(f"{name} {description}")),
|
||||
"risk_indicators": scan_indicators(skill_file.parent),
|
||||
"issues": issues,
|
||||
}
|
||||
|
||||
|
||||
def find_overlaps(skills: list[dict[str, object]], threshold: float, limit: int) -> list[dict[str, object]]:
|
||||
overlaps: list[dict[str, object]] = []
|
||||
for i, left in enumerate(skills):
|
||||
left_tokens = set(left.get("trigger_tokens", []))
|
||||
if not left_tokens:
|
||||
continue
|
||||
for right in skills[i + 1:]:
|
||||
right_tokens = set(right.get("trigger_tokens", []))
|
||||
shared = left_tokens & right_tokens
|
||||
union = left_tokens | right_tokens
|
||||
if len(shared) < 3 or not union:
|
||||
continue
|
||||
score = len(shared) / len(union)
|
||||
if score >= threshold:
|
||||
overlaps.append({
|
||||
"left": left.get("name") or left.get("path"),
|
||||
"right": right.get("name") or right.get("path"),
|
||||
"score": round(score, 3),
|
||||
"shared_terms": sorted(shared)[:20],
|
||||
})
|
||||
overlaps.sort(key=lambda item: (-float(item["score"]), str(item["left"]), str(item["right"])))
|
||||
return overlaps[:limit]
|
||||
|
||||
|
||||
def render_markdown(report: dict[str, object]) -> str:
|
||||
summary = report["summary"]
|
||||
lines = [
|
||||
"# Skill inventory",
|
||||
"",
|
||||
f"- Skills found: {summary['skills_found']}",
|
||||
f"- Duplicate names: {summary['duplicate_names']}",
|
||||
f"- Trigger overlaps reported: {summary['trigger_overlaps']}",
|
||||
f"- Skills with indicators: {summary['skills_with_risk_indicators']}",
|
||||
"",
|
||||
"| Skill | Root | Issues | Indicators |",
|
||||
"|---|---|---:|---:|",
|
||||
]
|
||||
for skill in report.get("skills", []):
|
||||
lines.append(
|
||||
f"| {skill.get('name') or '(invalid)'} | {skill.get('root')} | "
|
||||
f"{len(skill.get('issues', []))} | {len(skill.get('risk_indicators', []))} |"
|
||||
)
|
||||
if report["duplicates"]:
|
||||
lines.extend(["", "## Duplicate names", "", "```json", json.dumps(report["duplicates"], ensure_ascii=False, indent=2), "```"])
|
||||
if report["overlaps"]:
|
||||
lines.extend(["", "## Trigger overlaps", "", "```json", json.dumps(report["overlaps"], ensure_ascii=False, indent=2), "```"])
|
||||
lines.extend(["", "> Indicators require manual review; they are not a malware verdict."])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", action="append", required=True, help="Skill root; repeat for multiple roots")
|
||||
parser.add_argument("--format", choices=("json", "markdown"), default="json")
|
||||
parser.add_argument("--overlap-threshold", type=float, default=0.28)
|
||||
parser.add_argument("--max-overlaps", type=int, default=200)
|
||||
parser.add_argument("--summary-only", action="store_true", help="Omit per-skill records from output")
|
||||
args = parser.parse_args()
|
||||
|
||||
roots: list[Path] = []
|
||||
missing_roots: list[str] = []
|
||||
for raw in args.root:
|
||||
root = Path(os.path.expandvars(os.path.expanduser(raw))).resolve()
|
||||
if root.is_dir():
|
||||
roots.append(root)
|
||||
else:
|
||||
missing_roots.append(str(root))
|
||||
|
||||
records: list[dict[str, object]] = []
|
||||
seen_paths: set[Path] = set()
|
||||
for root in roots:
|
||||
for skill_file in iter_skill_files(root):
|
||||
resolved = skill_file.resolve()
|
||||
if resolved in seen_paths:
|
||||
continue
|
||||
seen_paths.add(resolved)
|
||||
records.append(skill_record(skill_file, root))
|
||||
records.sort(key=lambda item: (str(item.get("name", "")), str(item.get("path", ""))))
|
||||
|
||||
by_name: dict[str, list[str]] = {}
|
||||
for record in records:
|
||||
name = str(record.get("name", ""))
|
||||
if name:
|
||||
by_name.setdefault(name, []).append(str(record["path"]))
|
||||
duplicates = {name: paths for name, paths in sorted(by_name.items()) if len(paths) > 1}
|
||||
overlaps = find_overlaps(records, args.overlap_threshold, args.max_overlaps)
|
||||
|
||||
report: dict[str, object] = {
|
||||
"roots": [str(root) for root in roots],
|
||||
"missing_roots": missing_roots,
|
||||
"summary": {
|
||||
"skills_found": len(records),
|
||||
"duplicate_names": len(duplicates),
|
||||
"trigger_overlaps": len(overlaps),
|
||||
"skills_with_risk_indicators": sum(bool(r.get("risk_indicators")) for r in records),
|
||||
},
|
||||
"duplicates": duplicates,
|
||||
"overlaps": overlaps,
|
||||
"skills": records,
|
||||
"notice": "Risk indicators and trigger overlap require manual review; they are not verdicts.",
|
||||
}
|
||||
if args.summary_only:
|
||||
report.pop("skills")
|
||||
if args.format == "markdown":
|
||||
print(render_markdown(report))
|
||||
else:
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preview or create a project-local Skill Stack routing profile."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SKILL_NAME = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
|
||||
|
||||
def validate_skill_name(value: str) -> str:
|
||||
if not SKILL_NAME.fullmatch(value):
|
||||
raise argparse.ArgumentTypeError(f"invalid Skill name: {value!r}")
|
||||
return value
|
||||
|
||||
|
||||
def parse_route(raw: str, active: set[str]) -> dict[str, object]:
|
||||
if "=" not in raw:
|
||||
raise ValueError(f"route must use 'intent=primary[,helper]': {raw!r}")
|
||||
intent, raw_skills = raw.split("=", 1)
|
||||
intent = intent.strip()
|
||||
skills = [item.strip() for item in raw_skills.split(",") if item.strip()]
|
||||
if not intent or not skills:
|
||||
raise ValueError(f"route has no intent or Skill: {raw!r}")
|
||||
invalid = [name for name in skills if not SKILL_NAME.fullmatch(name)]
|
||||
if invalid:
|
||||
raise ValueError("route contains invalid Skill names: " + ", ".join(invalid))
|
||||
missing = [name for name in skills if name not in active]
|
||||
if missing:
|
||||
raise ValueError("route refers to Skills not listed with --skill: " + ", ".join(missing))
|
||||
return {
|
||||
"intent": intent,
|
||||
"primary": skills[0],
|
||||
"supporting": skills[1:],
|
||||
}
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, payload: dict[str, object]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
temporary = handle.name
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--project", required=True, help="Project root")
|
||||
parser.add_argument("--name", required=True, help="Plain profile name")
|
||||
parser.add_argument("--skill", action="append", required=True, type=validate_skill_name, help="Active Skill; repeatable")
|
||||
parser.add_argument("--route", action="append", default=[], help="Intent route: intent=primary[,helper]")
|
||||
parser.add_argument("--strict", action="store_true", help="Do not search outside this profile automatically")
|
||||
parser.add_argument("--apply", action="store_true", help="Write the profile; default is preview only")
|
||||
parser.add_argument("--update", action="store_true", help="Replace an existing profile; requires --apply")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.update and not args.apply:
|
||||
raise SystemExit("--update requires --apply")
|
||||
|
||||
project = Path(os.path.expandvars(os.path.expanduser(args.project))).resolve()
|
||||
if project.is_symlink() or not project.is_dir():
|
||||
raise SystemExit(f"project is not a regular directory: {project}")
|
||||
|
||||
active_skills = list(dict.fromkeys(args.skill))
|
||||
active_set = set(active_skills)
|
||||
try:
|
||||
routes = [parse_route(raw, active_set) for raw in args.route]
|
||||
except ValueError as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
|
||||
profile_path = project / ".codex" / "skill-stack.json"
|
||||
if profile_path.exists() and not args.update:
|
||||
raise SystemExit(f"profile already exists; refusing to overwrite: {profile_path}")
|
||||
|
||||
payload: dict[str, object] = {
|
||||
"schema": 1,
|
||||
"profile_name": args.name.strip(),
|
||||
"project_root": str(project),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"active_skills": active_skills,
|
||||
"routes": routes,
|
||||
"routing": {
|
||||
"preference": "profile-first",
|
||||
"outside_search": "never" if args.strict else "only-for-uncovered-capabilities",
|
||||
},
|
||||
"privacy": "This profile stores routing preferences only. It contains no prompts, usage history, or feedback logs.",
|
||||
"technical_note": "A profile guides routing. Actual hard scoping requires project-local Skill installation when supported by the client.",
|
||||
}
|
||||
|
||||
if args.apply:
|
||||
atomic_write_json(profile_path, payload)
|
||||
status = "updated" if args.update else "created"
|
||||
else:
|
||||
status = "preview"
|
||||
|
||||
print(json.dumps({
|
||||
"status": status,
|
||||
"profile_path": str(profile_path),
|
||||
"profile": payload,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a safe, dependency-free SVG recommendation card from JSON."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
STATUS_COLORS = {
|
||||
"available": ("#0f766e", "#ccfbf1"),
|
||||
"recommended": ("#1d4ed8", "#dbeafe"),
|
||||
"optional": ("#7c3aed", "#ede9fe"),
|
||||
"not-recommended": ("#b45309", "#fef3c7"),
|
||||
"verified": ("#15803d", "#dcfce7"),
|
||||
}
|
||||
|
||||
|
||||
def clean_text(value: object, limit: int) -> str:
|
||||
text = " ".join(str(value or "").split())
|
||||
return text[:limit]
|
||||
|
||||
|
||||
def wrap(value: object, width: int, limit: int) -> list[str]:
|
||||
text = clean_text(value, limit)
|
||||
return textwrap.wrap(text, width=width, break_long_words=False) or [""]
|
||||
|
||||
|
||||
def atomic_write(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
||||
handle.write(content)
|
||||
temporary = handle.name
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def validate(payload: object) -> dict[str, object]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("card input must be a JSON object")
|
||||
if not clean_text(payload.get("title"), 120):
|
||||
raise ValueError("title is required")
|
||||
if not clean_text(payload.get("goal"), 400):
|
||||
raise ValueError("goal is required")
|
||||
skills = payload.get("skills")
|
||||
if not isinstance(skills, list) or not skills:
|
||||
raise ValueError("skills must be a non-empty list")
|
||||
if len(skills) > 8:
|
||||
raise ValueError("a shareable card supports at most 8 Skills")
|
||||
for index, skill in enumerate(skills):
|
||||
if not isinstance(skill, dict) or not clean_text(skill.get("name"), 80):
|
||||
raise ValueError(f"skills[{index}].name is required")
|
||||
return payload
|
||||
|
||||
|
||||
def text_element(x: int, y: int, text: object, size: int, color: str, weight: int = 400) -> str:
|
||||
return (
|
||||
f'<text x="{x}" y="{y}" font-family="Inter, ui-sans-serif, system-ui, sans-serif" '
|
||||
f'font-size="{size}" font-weight="{weight}" fill="{color}">{html.escape(str(text))}</text>'
|
||||
)
|
||||
|
||||
|
||||
def render(payload: dict[str, object]) -> str:
|
||||
width = 1200
|
||||
title = clean_text(payload.get("title"), 120)
|
||||
goal_lines = wrap(payload.get("goal"), 82, 400)[:3]
|
||||
skills = payload["skills"]
|
||||
warnings = payload.get("boundaries", [])
|
||||
if not isinstance(warnings, list):
|
||||
warnings = [warnings]
|
||||
warning_lines: list[str] = []
|
||||
for warning in warnings[:3]:
|
||||
warning_lines.extend(wrap(warning, 92, 240)[:2])
|
||||
height = 250 + len(goal_lines) * 34 + len(skills) * 92 + max(1, len(warning_lines)) * 30 + 110
|
||||
|
||||
parts = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img" aria-labelledby="title desc">',
|
||||
f'<title id="title">{html.escape(title)}</title>',
|
||||
f'<desc id="desc">{html.escape(clean_text(payload.get("goal"), 400))}</desc>',
|
||||
'<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#07152f"/><stop offset="1" stop-color="#123b5d"/></linearGradient></defs>',
|
||||
f'<rect width="{width}" height="{height}" rx="36" fill="url(#bg)"/>',
|
||||
'<circle cx="1080" cy="90" r="160" fill="#38bdf8" opacity="0.10"/>',
|
||||
'<circle cx="1120" cy="30" r="80" fill="#a78bfa" opacity="0.12"/>',
|
||||
text_element(64, 72, "AGENT SKILL STACK", 22, "#7dd3fc", 700),
|
||||
text_element(64, 122, title, 38, "#ffffff", 750),
|
||||
]
|
||||
y = 166
|
||||
for line in goal_lines:
|
||||
parts.append(text_element(64, y, line, 24, "#dbeafe", 400))
|
||||
y += 34
|
||||
y += 22
|
||||
|
||||
for skill in skills:
|
||||
name = clean_text(skill.get("name"), 80)
|
||||
role = clean_text(skill.get("role"), 180)
|
||||
status = clean_text(skill.get("status"), 40).lower() or "recommended"
|
||||
foreground, background = STATUS_COLORS.get(status, ("#334155", "#e2e8f0"))
|
||||
parts.extend([
|
||||
f'<rect x="56" y="{y}" width="1088" height="72" rx="18" fill="#ffffff" opacity="0.96"/>',
|
||||
text_element(84, y + 31, name, 24, "#0f172a", 700),
|
||||
text_element(84, y + 57, role, 18, "#475569", 400),
|
||||
f'<rect x="956" y="{y + 18}" width="160" height="36" rx="18" fill="{background}"/>',
|
||||
text_element(976, y + 43, status.replace("-", " ").title(), 16, foreground, 700),
|
||||
])
|
||||
y += 92
|
||||
|
||||
parts.append(text_element(64, y + 4, "SAFETY BOUNDARY", 18, "#7dd3fc", 700))
|
||||
y += 34
|
||||
if not warning_lines:
|
||||
warning_lines = ["No additional boundary recorded."]
|
||||
for line in warning_lines:
|
||||
parts.append(text_element(72, y, f"• {line}", 19, "#e2e8f0", 400))
|
||||
y += 30
|
||||
|
||||
verified = clean_text(payload.get("verified"), 40) or "not recorded"
|
||||
footer = clean_text(payload.get("footer"), 120) or "Minimal. Audited. Project-specific."
|
||||
parts.extend([
|
||||
f'<line x1="64" y1="{height - 78}" x2="1136" y2="{height - 78}" stroke="#7dd3fc" opacity="0.25"/>',
|
||||
text_element(64, height - 38, footer, 17, "#bae6fd", 500),
|
||||
text_element(934, height - 38, f"Verified: {verified}", 17, "#bae6fd", 500),
|
||||
"</svg>",
|
||||
])
|
||||
return "\n".join(parts) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--input", required=True, help="JSON card definition")
|
||||
parser.add_argument("--output", required=True, help="SVG destination")
|
||||
parser.add_argument("--force", action="store_true", help="Replace an existing output file")
|
||||
args = parser.parse_args()
|
||||
|
||||
source = Path(args.input).expanduser().resolve()
|
||||
output = Path(args.output).expanduser().resolve()
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"input is not a file: {source}")
|
||||
if output.exists() and not args.force:
|
||||
raise SystemExit(f"refusing to overwrite existing output: {output}")
|
||||
if output.suffix.lower() != ".svg":
|
||||
raise SystemExit("output must use the .svg extension")
|
||||
|
||||
try:
|
||||
payload = validate(json.loads(source.read_text(encoding="utf-8")))
|
||||
svg = render(payload)
|
||||
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
atomic_write(output, svg)
|
||||
print(json.dumps({"status": "created", "output": str(output)}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and search a local, read-only index of installed agent Skills."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from inventory_skills import iter_skill_files, parse_frontmatter, tokenize
|
||||
|
||||
|
||||
QUERY_EXPANSIONS = [
|
||||
(
|
||||
("技能组合", "技能栈", "技能包", "配齐", "skill stack", "一套skills", "一套 skills"),
|
||||
"agent-skill-stack build curate skill stack capability workflow project profile local index compare conflicts install 组合 技能栈 能力 工作流 项目",
|
||||
),
|
||||
(
|
||||
("找一个", "找个", "找一款", "find a skill", "有没有skill", "有没有 skill", "有没有能"),
|
||||
"find-skills find skills discover install common capability 查找 单个 技能",
|
||||
),
|
||||
(
|
||||
("去ai", "ai味", "humanize", "natural writing", "文风", "自然一点"),
|
||||
"humanizer humanize writing rewrite natural style tone voice 文案 改写 自然 文风",
|
||||
),
|
||||
(
|
||||
("事实核查", "fact check", "引用", "citation", "可信"),
|
||||
"fact check verify evidence citation grounded accuracy 核查 引用 证据 准确",
|
||||
),
|
||||
(
|
||||
("合规", "compliance", "版权", "copyright", "规则"),
|
||||
"compliance policy copyright legal safety audit 合规 版权 规则 审核",
|
||||
),
|
||||
(
|
||||
("调研", "research", "对标", "竞品", "搜集"),
|
||||
"research search collect compare benchmark competitor evidence 调研 搜索 收集 对标 竞品",
|
||||
),
|
||||
(
|
||||
("发布", "publish", "定时", "schedule"),
|
||||
"publish schedule post upload automation approval 发布 定时 上传 自动化 审批",
|
||||
),
|
||||
(
|
||||
("整理", "入库", "知识库", "organize", "knowledge base"),
|
||||
"organize knowledge base notes database deduplicate structure 整理 入库 知识库 去重 结构化",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def utc_iso(timestamp: float | None = None) -> str:
|
||||
moment = datetime.fromtimestamp(timestamp, tz=timezone.utc) if timestamp is not None else datetime.now(timezone.utc)
|
||||
return moment.isoformat()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, payload: dict[str, object]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
temporary = handle.name
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def infer_scope(skill_file: Path, project_root: Path | None) -> str:
|
||||
if project_root is not None:
|
||||
try:
|
||||
skill_file.relative_to(project_root)
|
||||
return "project"
|
||||
except ValueError:
|
||||
pass
|
||||
return "global"
|
||||
|
||||
|
||||
def build_record(skill_file: Path, root: Path, project_root: Path | None) -> dict[str, object]:
|
||||
text = skill_file.read_text(encoding="utf-8", errors="replace")
|
||||
metadata, issues = parse_frontmatter(text[:300_000])
|
||||
name = metadata.get("name", "")
|
||||
description = metadata.get("description", "")
|
||||
headings = [
|
||||
re.sub(r"\s+#+$", "", heading).strip()
|
||||
for heading in re.findall(r"^#{1,3}\s+(.+)$", text, flags=re.MULTILINE)
|
||||
][:40]
|
||||
capability_terms = sorted(tokenize(" ".join([name, description, *headings])))
|
||||
if name and skill_file.parent.name != name:
|
||||
issues.append(f"directory name '{skill_file.parent.name}' differs from skill name '{name}'")
|
||||
fingerprint = sha256_file(skill_file)
|
||||
summary = re.sub(r"\s+", " ", description).strip()
|
||||
if len(summary) > 360:
|
||||
summary = summary[:357].rstrip() + "..."
|
||||
return {
|
||||
"id": f"{name or 'invalid'}:{fingerprint[:12]}",
|
||||
"name": name,
|
||||
"display_name": name.replace("-", " ").strip().title() if name else "Invalid Skill",
|
||||
"summary": summary,
|
||||
"scope": infer_scope(skill_file, project_root),
|
||||
"aliases": capability_terms[:80],
|
||||
"capability_terms": capability_terms,
|
||||
"headings": headings,
|
||||
"last_local_change": utc_iso(skill_file.stat().st_mtime),
|
||||
"issues": issues,
|
||||
"technical": {
|
||||
"source_root": str(root),
|
||||
"path": str(skill_file.parent),
|
||||
"skill_file_fingerprint": fingerprint,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_index(args: argparse.Namespace) -> int:
|
||||
project_root = Path(args.project_root).expanduser().resolve() if args.project_root else None
|
||||
roots: list[Path] = []
|
||||
missing: list[str] = []
|
||||
for raw in args.root:
|
||||
root = Path(os.path.expandvars(os.path.expanduser(raw))).resolve()
|
||||
if root.is_dir():
|
||||
roots.append(root)
|
||||
else:
|
||||
missing.append(str(root))
|
||||
|
||||
records: list[dict[str, object]] = []
|
||||
seen: set[Path] = set()
|
||||
for root in roots:
|
||||
for skill_file in iter_skill_files(root):
|
||||
resolved = skill_file.resolve()
|
||||
if resolved in seen:
|
||||
continue
|
||||
seen.add(resolved)
|
||||
records.append(build_record(skill_file, root, project_root))
|
||||
records.sort(key=lambda item: (str(item.get("name", "")), str(item["technical"]["path"])))
|
||||
|
||||
names: dict[str, list[str]] = {}
|
||||
for record in records:
|
||||
if record["name"]:
|
||||
names.setdefault(str(record["name"]), []).append(str(record["id"]))
|
||||
duplicates = {name: ids for name, ids in sorted(names.items()) if len(ids) > 1}
|
||||
|
||||
output = Path(os.path.expandvars(os.path.expanduser(args.output))).resolve()
|
||||
payload: dict[str, object] = {
|
||||
"schema": 1,
|
||||
"generated_at": utc_iso(),
|
||||
"roots": [str(root) for root in roots],
|
||||
"missing_roots": missing,
|
||||
"skills": records,
|
||||
"duplicates": duplicates,
|
||||
"privacy": "This index stores Skill metadata only. It contains no prompts, usage history, or routing feedback.",
|
||||
}
|
||||
atomic_write_json(output, payload)
|
||||
print(json.dumps({
|
||||
"status": "built",
|
||||
"output": str(output),
|
||||
"skills_indexed": len(records),
|
||||
"duplicate_names": len(duplicates),
|
||||
"missing_roots": missing,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def expanded_query(query: str) -> str:
|
||||
lower = query.lower()
|
||||
additions = [terms for triggers, terms in QUERY_EXPANSIONS if any(trigger in lower for trigger in triggers)]
|
||||
return " ".join([query, *additions])
|
||||
|
||||
|
||||
def score_record(
|
||||
record: dict[str, object],
|
||||
query: str,
|
||||
direct_tokens: set[str],
|
||||
expanded_tokens: set[str],
|
||||
) -> tuple[float, list[str]]:
|
||||
name = str(record.get("name", "")).lower()
|
||||
summary = str(record.get("summary", "")).lower()
|
||||
aliases = set(str(item) for item in record.get("aliases", []))
|
||||
capability_terms = set(str(item) for item in record.get("capability_terms", []))
|
||||
lower_query = query.lower().strip()
|
||||
record_tokens = aliases | capability_terms
|
||||
direct_matched = sorted(direct_tokens & record_tokens)
|
||||
helper_matched = sorted((expanded_tokens - direct_tokens) & record_tokens)
|
||||
matched = [*direct_matched, *helper_matched]
|
||||
|
||||
# What the user actually said must outrank generic query-expansion terms.
|
||||
score = float(len(direct_matched) * 4 + len(helper_matched))
|
||||
if lower_query and lower_query == name:
|
||||
score += 20
|
||||
elif lower_query and lower_query in name:
|
||||
score += 10
|
||||
if lower_query and lower_query in summary:
|
||||
score += 8
|
||||
if name and name in direct_tokens:
|
||||
score += 18
|
||||
elif name and name in expanded_tokens:
|
||||
score += 14
|
||||
if direct_tokens:
|
||||
score += 10 * len(direct_matched) / len(direct_tokens)
|
||||
if score > 0 and record.get("scope") == "project":
|
||||
score += 1
|
||||
if record.get("issues"):
|
||||
score -= 2
|
||||
return score, matched
|
||||
|
||||
|
||||
def search_index(args: argparse.Namespace) -> int:
|
||||
index_path = Path(os.path.expandvars(os.path.expanduser(args.index))).resolve()
|
||||
payload = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
expanded = expanded_query(args.query)
|
||||
direct_tokens = tokenize(args.query)
|
||||
expanded_tokens = tokenize(expanded)
|
||||
results: list[dict[str, object]] = []
|
||||
for record in payload.get("skills", []):
|
||||
score, matched = score_record(record, args.query, direct_tokens, expanded_tokens)
|
||||
if score <= 0:
|
||||
continue
|
||||
results.append({
|
||||
"name": record.get("name"),
|
||||
"display_name": record.get("display_name"),
|
||||
"summary": record.get("summary"),
|
||||
"scope": record.get("scope"),
|
||||
"score": round(score, 3),
|
||||
"matched_terms": matched[:20],
|
||||
"issues": record.get("issues", []),
|
||||
"technical": record.get("technical", {}),
|
||||
})
|
||||
results.sort(key=lambda item: (-float(item["score"]), str(item["name"])))
|
||||
results = results[:args.limit]
|
||||
|
||||
if args.format == "simple":
|
||||
for position, result in enumerate(results, start=1):
|
||||
scope = "项目内" if result["scope"] == "project" else "全局"
|
||||
print(f"{position}. {result['display_name']}({scope})— {result['summary']}")
|
||||
else:
|
||||
print(json.dumps({
|
||||
"query": args.query,
|
||||
"expanded_query": expanded,
|
||||
"results": results,
|
||||
"notice": "Search scores are retrieval hints, not quality or installation scores.",
|
||||
}, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
build = subparsers.add_parser("build", help="Build or refresh a local Skill index")
|
||||
build.add_argument("--root", action="append", required=True, help="Skill root; repeatable")
|
||||
build.add_argument("--output", required=True, help="Index JSON output path")
|
||||
build.add_argument("--project-root", help="Optional project root used to mark project-scoped Skills")
|
||||
build.set_defaults(handler=build_index)
|
||||
|
||||
search = subparsers.add_parser("search", help="Search a previously built local Skill index")
|
||||
search.add_argument("--index", required=True, help="Index JSON path")
|
||||
search.add_argument("--query", required=True, help="Natural-language capability query")
|
||||
search.add_argument("--limit", type=int, default=10)
|
||||
search.add_argument("--format", choices=("json", "simple"), default="json")
|
||||
search.set_defaults(handler=search_index)
|
||||
|
||||
args = parser.parse_args()
|
||||
return int(args.handler(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preview or install already downloaded and audited skill directories without overwrite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
|
||||
|
||||
def parse_name(skill_file: Path) -> str:
|
||||
lines = skill_file.read_text(encoding="utf-8", errors="strict").splitlines()
|
||||
if not lines or lines[0].strip() != "---":
|
||||
raise ValueError(f"{skill_file}: missing opening frontmatter delimiter")
|
||||
end = next((i for i in range(1, len(lines)) if lines[i].strip() == "---"), None)
|
||||
if end is None:
|
||||
raise ValueError(f"{skill_file}: missing closing frontmatter delimiter")
|
||||
for line in lines[1:end]:
|
||||
match = re.match(r"^name:\s*([^#]+?)\s*$", line)
|
||||
if match:
|
||||
name = match.group(1).strip().strip('"\'')
|
||||
if len(name) > 63 or not NAME_RE.fullmatch(name):
|
||||
raise ValueError(f"{skill_file}: invalid skill name {name!r}")
|
||||
return name
|
||||
raise ValueError(f"{skill_file}: missing name")
|
||||
|
||||
|
||||
def collect_files(source: Path) -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for current, dirs, filenames in os.walk(source, followlinks=False):
|
||||
current_path = Path(current)
|
||||
for dirname in dirs:
|
||||
path = current_path / dirname
|
||||
if path.is_symlink():
|
||||
raise ValueError(f"symlinked directories are not allowed: {path}")
|
||||
for filename in filenames:
|
||||
path = current_path / filename
|
||||
if path.is_symlink():
|
||||
raise ValueError(f"symlinked files are not allowed: {path}")
|
||||
if not path.is_file():
|
||||
raise ValueError(f"unsupported filesystem entry: {path}")
|
||||
files.append(path)
|
||||
return sorted(files, key=lambda path: str(path.relative_to(source)))
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def source_record(source: Path, dest: Path) -> dict[str, object]:
|
||||
if source.is_symlink() or not source.is_dir():
|
||||
raise ValueError(f"source is not a regular directory: {source}")
|
||||
skill_file = source / "SKILL.md"
|
||||
if not skill_file.is_file():
|
||||
raise ValueError(f"source has no SKILL.md: {source}")
|
||||
name = parse_name(skill_file)
|
||||
files = collect_files(source)
|
||||
file_records = [
|
||||
{
|
||||
"path": str(path.relative_to(source)),
|
||||
"sha256": sha256_file(path),
|
||||
"bytes": path.stat().st_size,
|
||||
}
|
||||
for path in files
|
||||
]
|
||||
aggregate = hashlib.sha256()
|
||||
for item in file_records:
|
||||
aggregate.update(str(item["path"]).encode("utf-8"))
|
||||
aggregate.update(str(item["sha256"]).encode("ascii"))
|
||||
return {
|
||||
"name": name,
|
||||
"source": str(source),
|
||||
"target": str(dest / name),
|
||||
"content_sha256": aggregate.hexdigest(),
|
||||
"file_count": len(file_records),
|
||||
"files": file_records,
|
||||
}
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, payload: dict[str, object]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
temp_name = handle.name
|
||||
os.replace(temp_name, path)
|
||||
|
||||
|
||||
def apply_install(records: list[dict[str, object]], dest: Path) -> list[str]:
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
staging = Path(tempfile.mkdtemp(prefix=".agent-skill-stack-", dir=dest))
|
||||
created: list[str] = []
|
||||
try:
|
||||
for record in records:
|
||||
source = Path(str(record["source"]))
|
||||
staged = staging / str(record["name"])
|
||||
shutil.copytree(source, staged, symlinks=False)
|
||||
if parse_name(staged / "SKILL.md") != record["name"]:
|
||||
raise RuntimeError(f"staged validation failed for {record['name']}")
|
||||
for record in records:
|
||||
staged = staging / str(record["name"])
|
||||
target = Path(str(record["target"]))
|
||||
os.replace(staged, target)
|
||||
created.append(str(target))
|
||||
return created
|
||||
finally:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", action="append", required=True, help="Audited local skill directory; repeatable")
|
||||
parser.add_argument("--dest", required=True, help="Destination skill root")
|
||||
parser.add_argument("--manifest", required=True, help="Path for the lock/preview manifest")
|
||||
parser.add_argument("--apply", action="store_true", help="Copy after validation; default is dry-run")
|
||||
parser.add_argument(
|
||||
"--record-existing",
|
||||
action="store_true",
|
||||
help="Record a lock manifest when each source is already its exact destination",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.apply and args.record_existing:
|
||||
raise SystemExit("--apply and --record-existing are mutually exclusive")
|
||||
|
||||
dest = Path(os.path.expandvars(os.path.expanduser(args.dest))).resolve()
|
||||
manifest = Path(os.path.expandvars(os.path.expanduser(args.manifest))).resolve()
|
||||
sources = [Path(os.path.expandvars(os.path.expanduser(raw))).resolve() for raw in args.source]
|
||||
|
||||
records = [source_record(source, dest) for source in sources]
|
||||
names = [str(record["name"]) for record in records]
|
||||
if len(names) != len(set(names)):
|
||||
raise SystemExit("duplicate skill names in selected sources")
|
||||
|
||||
existing = [str(record["target"]) for record in records if Path(str(record["target"])).exists()]
|
||||
if args.record_existing:
|
||||
mismatched = [
|
||||
str(record["name"])
|
||||
for record in records
|
||||
if Path(str(record["source"])).resolve() != Path(str(record["target"])).resolve()
|
||||
]
|
||||
if mismatched:
|
||||
raise SystemExit("--record-existing requires source to equal target for: " + ", ".join(mismatched))
|
||||
elif existing:
|
||||
raise SystemExit("refusing to overwrite existing destinations: " + ", ".join(existing))
|
||||
|
||||
payload: dict[str, object] = {
|
||||
"schema": 1,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"mode": "record-existing" if args.record_existing else ("apply" if args.apply else "dry-run"),
|
||||
"destination": str(dest),
|
||||
"skills": records,
|
||||
"created": [],
|
||||
"notice": "Sources must be downloaded and audited before using this installer. Existing targets are never overwritten.",
|
||||
}
|
||||
|
||||
if args.record_existing:
|
||||
payload["status"] = "recorded"
|
||||
elif args.apply:
|
||||
payload["created"] = apply_install(records, dest)
|
||||
payload["status"] = "installed"
|
||||
else:
|
||||
payload["status"] = "planned"
|
||||
|
||||
atomic_write_json(manifest, payload)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user