lov-better-css
清理冗余 CSS 并在现有 Tailwind 项目中重构样式。支持明确输入与结果回读。Use to refactor CSS and Tailwind
Install
npx skills add https://github.com/lovstudio/skills/tree/main/skills/better-css
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
git clone https://github.com/lovstudio/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.
README
CSS 精修 · CSS Polish
清理冗余 CSS 并在现有 Tailwind 项目中重构样式。
安装
npx skills add lov-better-css -g -y
使用
- 清理冗余 CSS 并在现有 Tailwind 项目中重构样式。
- Refactor CSS and Tailwind styles.
支持自然语言调用,具体执行步骤与边界见 SKILL.md。分析、预览和实际修改分开处理。
配置与依赖
读取 共享 Profile;项目路径、输出目录与品牌来自当前请求或用户配置。 业务工具和目标系统依赖见工作流;公开文案使用 lov-branding-consistency 审校。
质量与案例
python3 scripts/validate_skill.py .
案例 记录真实迁移输入和输出,仅证明所声明的验收范围。 Skill Card 与 定价依据 说明使用边界;业务运行结果须逐次回读。
License
MIT
Skill manifest
CSS 精修 · CSS Polish
清理冗余 CSS 并在现有 Tailwind 项目中重构样式。
Triggers
Activate when
The user asks to use this Skill for its documented outcome.
“清理冗余 CSS 并在现有 Tailwind 项目中重构样式。”
“Refactor CSS and Tailwind styles.”
Do not activate when
- 只是查询本 Skill 的说明,或请求与上述结果无关的任务;不执行实际业务操作。
- 用户仅要预览或审查时,不进入修改、提交或发布分支。
Execution boundary
自然语言请求即可触发;无需旧 slash 路径、参数插值或指定助手。明确解析当前请求中的 项目、目标文件、选项与输出位置;用当前宿主实际提供的文件、搜索、CLI 和浏览器能力。 项目依赖版本与外部 API 在执行时核实,不能假设示例是现行配置。随包脚本从 Skill 根解析, 业务文件从目标项目根解析。先读当前状态,保护已有未提交内容与其他任务的暂存区。 分析、预览请求保持只读;修改、提交、推送、部署和发布各依当前请求的明确范围执行。 不绕过保护、自动发送消息、强制结束用户进程或抢前台。失败保留可诊断原始错误。
Workflow
读取指定样式与组件,记录动态 class、第三方约定、主题 token 和当前 Tailwind 版本。仅静态搜索无引用不能证明动态样式未使用。
按重复、覆盖、未使用和可迁移样式建立修改清单;保留动画、伪元素、滚动条和必要覆盖规则。项目未采用 Tailwind 时不强行引入。
逐组件合并样式并更新引用;遵循既有 semantic token,保持响应式、暗色、焦点与交互状态。用户要求 dry-run 时只交付差异方案。
运行相关构建或类型检查;在可用页面对照关键断点和状态。报告实际体积变化,未测量时不填虚构百分比。
Composition
执行前读取 能力组合,按明确制品交接相邻能力。
Runtime context (shared)
运行前读取本包 skill.yaml 与 Profile 合同。优先级为当前请求、
项目上下文、本 Skill records、共享 preferences、brand/user Profile、安全默认值。
只读取声明字段;没有专用运行时的宿主可使用 scripts/profile_store.py 读取共享 Profile。
配置缺失只问影响结果的一个问题。用户明确要求长期保存的值通过该脚本原子写入,
报告实际路径;不保存推断、凭据或其他任务的资料。
Files (skills)
-
cases
-
cases.json 653 B
[ { "title": "历史命令通用化迁移验证", "description": "真实迁移请求的结构与可发现性验收;业务系统操作没有在本案例执行。", "input": { "source": "历史命令或本地已迁移 Skill", "sha256": "34f115b81a8e20d649014079d81126a8ce4976a94b70fd59b2ccd0f2b1c622dc" }, "prompt": "将本地所有历史 slash command 逐一升级,并全部同步官网。", "output": { "artifact": "SKILL.md", "result": "显式输入、自然语言触发、跨宿主执行边界及共享 Profile 合同", "limitation": "该案例不证明外部业务写入成功" } } ]
-
-
references
-
skill-composition.md 840 B
# Skill Group Composition ## Nearby Skills Inspected - lov-skill-creator:命令迁移与通用结构;不拥有本业务结果。 - lov-branding-consistency:用户可见文案审校。 - lov-skill-publisher:已验证 Skill 的官网分发。 ## Atomic Handoffs 本 Skill 接收请求与项目事实,交付清理冗余 CSS 并在现有 Tailwind 项目中重构样式。文案审校只消费公开文本;发布器只消费校验后的 Skill 源,不接管业务执行。 ## Overlap Decisions 旧命令按实际输入输出映射到本真源;不新增另一份重复业务实现。其他历史能力在本批次逐项比较,只有用户要求相邻结果时交接。 ## Composition Decision Single Skill:围绕同一结果的预览、执行和验证共享上下文;不把依赖工具或宿主适配误拆成 Kit。 -
user-profile.md 2.7 KB
# User Profile contract Every Skill created by Skill Creator declares `user-profile/v1` in `skill.yaml`. The contract connects independent sessions to one user-owned JSON Profile while keeping the Skill source portable across users and brands. ## Shared shape The host supplies the Profile through `SKILL_PROFILE_PATH` (or the runtime's configured profile path). The stable shared scopes are: - `user`: user identity, language, timezone, and other personal working defaults. - `brand`: public brand facts, site, logo, tone, profile, and design guidance. - `workspace`: project roots and output locations. - `preferences`: shared preference values when the host stores them in the Profile. - `skills.<skill_id>.profile`: Skill-specific defaults. - `skills.<skill_id>.records`: durable decisions and preferences learned from direct user statements for this Skill. The Profile may also use the runtime's canonical `identity` fields. Manifest field aliases bridge `identity.*` and the portable `user.*` / `brand.*` names. ## Read on every run 1. Read the current request and project context. 2. Read the shared Profile and the `skills.<skill_id>` namespace. 3. Resolve values in this order: current request, project context, Skill records, shared preferences, shared user/brand Profile, safe defaults. 4. Keep `profile_scope` and field provenance available for the final result. Do not copy resolved personal paths, brand values, or private records into the committed Skill source. ## Persist directly stated values When the user explicitly gives a value meant to survive later sessions, save it immediately after the user statement and report the canonical path: ```bash python3 scripts/profile_store.py record \ --skill-id lov-example \ --path records.subtitle_level \ --value '"cet4"' \ --confirm ``` For shared facts, use `--path brand.<field>` or `--path user.<field>`. The script writes JSON atomically, preserves unrelated Profile data, increments a numeric Profile revision when present, and never echoes the stored value. Inferred information, credentials, tokens, cookies, and secret-like fields stay out of durable records. If the user has not stated that a value should persist, keep it in the current request context. ## Read the connected context ```bash python3 scripts/profile_store.py read \ --skill-id lov-example \ --pretty ``` The result contains `user`, `brand`, `workspace`, `preferences`, `skill`, and `records` scopes. A host using `skill-runtime/v1` also returns the same binding as `profile_scope` and `profile_contract`. ## Compatibility `--user-config` remains accepted by the Creator as a compatibility flag for old invocations. The Profile contract is now always generated; users do not choose an initialization mode.
-
-
scripts
-
profile_store.py 8.3 KB
#!/usr/bin/env python3 """Read and persist a Skill's user-owned profile context. The command keeps shared identity and brand facts in the profile root while putting Skill-specific durable records under ``skills.<skill_id>.records``. Writes require an explicit confirmation flag and use an atomic replacement. """ from __future__ import annotations import argparse import json import os import re import sys import tempfile from pathlib import Path from typing import Any, Iterable USER_PROFILE_SCHEMA = "user-profile/v1" SKILL_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") SAFE_ROOTS = {"user", "brand", "workspace", "preferences"} SENSITIVE_PARTS = {"token", "secret", "password", "credential", "cookie", "api_key", "apikey"} def config_dir() -> Path: configured = os.environ.get("SKILLS_CONFIG_DIR") if configured: return Path(os.path.expandvars(configured)).expanduser() xdg = os.environ.get("XDG_CONFIG_HOME") if xdg: return Path(os.path.expandvars(xdg)).expanduser() / "agent-skills" return Path.home() / ".config" / "agent-skills" def profile_path(explicit: Path | None = None) -> Path: if explicit: return explicit.expanduser() configured = ( os.environ.get("SKILL_PROFILE_PATH") or os.environ.get("SKILLS_PROFILE_PATH") ) if configured: return Path(os.path.expandvars(configured)).expanduser() candidates = ( Path.home() / ".lovstudio" / "skills" / "profile.json", Path.home() / ".skill-publisher" / "skills" / "profile.json", config_dir() / "profile.json", ) return next((candidate for candidate in candidates if candidate.exists()), candidates[-1]) def read_profile(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise ValueError(f"profile cannot be read: {path}: {exc}") from exc if not isinstance(value, dict): raise ValueError("profile root must be an object") return value def profile_skeleton(skill_id: str) -> dict[str, Any]: return { "schema": "skill-profile/v1", "profile_id": f"profile-{skill_id}", "revision": 1, "status": "draft", "identity": {}, "purpose": {}, "brand": {}, "user": {}, "workspace": {}, "skills": {}, "extensions": {}, } def atomic_write(path: Path, value: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) mode = 0o600 if path.exists(): mode = path.stat().st_mode & 0o777 temporary_name: str | None = None try: with tempfile.NamedTemporaryFile( "w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False, ) as handle: temporary_name = handle.name handle.write(json.dumps(value, ensure_ascii=False, indent=2) + "\n") handle.flush() os.fsync(handle.fileno()) os.chmod(temporary_name, mode) os.replace(temporary_name, path) finally: if temporary_name: temporary = Path(temporary_name) if temporary.exists(): temporary.unlink() def set_path(root: dict[str, Any], parts: list[str], value: Any) -> None: current: dict[str, Any] = root for part in parts[:-1]: child = current.get(part) if not isinstance(child, dict): child = {} current[part] = child current = child current[parts[-1]] = value def target_parts(skill_id: str, requested_path: str) -> tuple[list[str], str]: if not SKILL_ID_RE.fullmatch(skill_id): raise ValueError("skill_id must be kebab-case") path = requested_path.strip().strip(".") if not path: raise ValueError("path is required") parts = [part for part in path.split(".") if part] if any(not re.fullmatch(r"[A-Za-z0-9_-]+", part) for part in parts): raise ValueError("path contains an invalid segment") if any(part.casefold() in SENSITIVE_PARTS for part in parts): raise ValueError("profile records do not accept secret-like fields") if parts[0] == "records": target = ["skills", skill_id, "records", *parts[1:]] return target, ".".join(target) if parts[0] == "profile": target = ["skills", skill_id, "profile", *parts[1:]] return target, ".".join(target) if parts[:3] == ["skills", skill_id, "records"]: return parts, ".".join(parts) if parts[:3] == ["skills", skill_id, "profile"]: return parts, ".".join(parts) if parts[0] in SAFE_ROOTS: return parts, ".".join(parts) expected = f"records.<field> or one of: {', '.join(sorted(SAFE_ROOTS))}.<field>" raise ValueError(f"path must use {expected}") def parse_value(raw: str) -> Any: try: return json.loads(raw) except json.JSONDecodeError: return raw def read_command(args: argparse.Namespace) -> dict[str, Any]: path = profile_path(args.profile) profile = read_profile(path) skills = profile.get("skills", {}) if not isinstance(skills, dict): skills = {} skill = skills.get(args.skill_id, {}) if not isinstance(skill, dict): skill = {} user = profile.get("user") if not isinstance(user, dict) or not user: user = profile.get("identity", {}) if not isinstance(user, dict): user = {} brand = profile.get("brand", {}) if not isinstance(brand, dict): brand = {} workspace = profile.get("workspace", {}) if not isinstance(workspace, dict): workspace = {} preferences = profile.get("preferences", {}) if not isinstance(preferences, dict): preferences = {} records = skill.get("records", {}) if not isinstance(records, dict): records = {} return { "status": "ready", "schema": USER_PROFILE_SCHEMA, "skill_id": args.skill_id, "profile_path": str(path), "user": user, "brand": brand, "workspace": workspace, "preferences": preferences, "skill": skill, "records": records, } def record_command(args: argparse.Namespace) -> dict[str, Any]: if not args.confirm: raise PermissionError("record requires --confirm after the user has stated the value") parts, canonical_path = target_parts(args.skill_id, args.path) path = profile_path(args.profile) profile = read_profile(path) if not profile: profile = profile_skeleton(args.skill_id) set_path(profile, parts, parse_value(args.value)) revision = profile.get("revision") if isinstance(revision, int) and not isinstance(revision, bool): profile["revision"] = revision + 1 atomic_write(path, profile) return { "status": "saved", "schema": USER_PROFILE_SCHEMA, "skill_id": args.skill_id, "profile_path": str(path), "path": canonical_path, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--profile", type=Path, default=None, help="Shared profile JSON path") subparsers = parser.add_subparsers(dest="command", required=True) read_parser = subparsers.add_parser("read", help="Read shared and Skill-specific profile context") read_parser.add_argument("--skill-id", required=True) read_parser.add_argument("--pretty", action="store_true") record_parser = subparsers.add_parser("record", help="Persist a user-stated profile value") record_parser.add_argument("--skill-id", required=True) record_parser.add_argument("--path", required=True, help="records.<field>, brand.<field>, or user.<field>") record_parser.add_argument("--value", required=True) record_parser.add_argument("--confirm", action="store_true") args = parser.parse_args() try: result = read_command(args) if args.command == "read" else record_command(args) except (OSError, PermissionError, ValueError) as exc: print(json.dumps({"status": "error", "error": str(exc)}, ensure_ascii=False), file=sys.stderr) return 2 indent = 2 if getattr(args, "pretty", False) else None print(json.dumps(result, ensure_ascii=False, indent=indent, sort_keys=bool(indent))) return 0 if __name__ == "__main__": raise SystemExit(main()) -
validate_skill.py 26.2 KB
#!/usr/bin/env python3 """Validate a portable local Skill Publisher Skill source directory.""" from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any, Iterable try: import yaml except ImportError: print( "ERROR: PyYAML is required. Install it with: python3 -m pip install PyYAML", file=sys.stderr, ) raise SystemExit(2) FRONTMATTER_KEYS = { "name", "description", "license", "compatibility", "allowed-tools", "depends_on", "metadata", } TEXT_SUFFIXES = {".md", ".json", ".yaml", ".yml", ".txt", ".svg", ".py"} JUNK_NAMES = {"__pycache__", ".DS_Store"} JUNK_SUFFIXES = {".pyc", ".pyo"} SKIP_DIRS = {".git", "dist", ".venv", "venv", "node_modules"} SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$") NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") MARKDOWN_LINK_RE = re.compile(r"!?\[[^\]]*]\(([^)]+)\)") SKILL_PATH_RE = re.compile(r"\$(SKILL_DIR|KIT_DIR)/([A-Za-z0-9_./-]+)") CARD_STANDARD = "lovstudio/skill-card/v1" PRICING_CARD_SCHEMA = "lovstudio/pricing-card/v1" MANIFEST_SCHEMA = "skill-manifest/v1" RUNTIME_VERSION = "skill-runtime/v1" PROFILE_SCHEMA = "user-profile/v1" CONTENT_CLASSES = { "authored-prose", "microcopy", "verbatim", "deterministic-output", } class ValidationFailure(Exception): """Raised when source metadata cannot be parsed.""" def read_text(path: Path) -> str: return path.read_text(encoding="utf-8") def compact_text(value: Any) -> str: return re.sub(r"\s+", " ", value).strip() if isinstance(value, str) else "" def split_frontmatter(path: Path) -> tuple[dict[str, Any], str]: text = read_text(path) if not text.startswith("---\n"): raise ValidationFailure(f"{path}: missing YAML frontmatter") marker = text.find("\n---\n", 4) if marker < 0: raise ValidationFailure(f"{path}: frontmatter is not closed") try: data = yaml.safe_load(text[4:marker]) except yaml.YAMLError as exc: raise ValidationFailure( f"{path}: standard YAML parser rejected frontmatter: {exc}" ) from exc if not isinstance(data, dict): raise ValidationFailure(f"{path}: frontmatter must be a mapping") return data, text[marker + 5 :] def iter_files(root: Path) -> Iterable[Path]: for path in root.rglob("*"): if any(part in SKIP_DIRS for part in path.relative_to(root).parts): continue if path.is_file(): yield path def is_relative_to(path: Path, parent: Path) -> bool: try: path.relative_to(parent) return True except ValueError: return False def validate_skill_file(path: Path, errors: list[str]) -> dict[str, Any] | None: try: data, body = split_frontmatter(path) except ValidationFailure as exc: errors.append(str(exc)) return None unexpected = sorted(set(data) - FRONTMATTER_KEYS) if unexpected: errors.append(f"{path}: unsupported frontmatter keys: {', '.join(unexpected)}") name = compact_text(data.get("name")) if not NAME_RE.fullmatch(name) or len(name) > 64: errors.append(f"{path}: name must be kebab-case and at most 64 characters") description = compact_text(data.get("description")) if not 50 <= len(description) <= 200: errors.append( f"{path}: description must contain 50-200 characters " f"(found {len(description)})" ) compatibility = compact_text(data.get("compatibility")) if not compatibility: errors.append(f"{path}: compatibility is required at the top level") depends_on = data.get("depends_on", []) if isinstance(depends_on, str): depends_on = [depends_on] if not isinstance(depends_on, list) or not all( isinstance(item, str) and NAME_RE.fullmatch(item) for item in depends_on ): errors.append(f"{path}: depends_on must be a list of exact Skill names") metadata = data.get("metadata") if not isinstance(metadata, dict): errors.append(f"{path}: metadata must be a mapping") else: if not compact_text(metadata.get("author")): errors.append(f"{path}: metadata.author is required") if not SEMVER_RE.fullmatch(compact_text(metadata.get("version"))): errors.append(f"{path}: metadata.version must use SemVer") tags = metadata.get("tags") if not isinstance(tags, list) or not tags or not all( isinstance(tag, str) and tag.strip() for tag in tags ): errors.append(f"{path}: metadata.tags must be a non-empty list") if "card_standard" in metadata and metadata.get("card_standard") != CARD_STANDARD: errors.append(f"{path}: metadata.card_standard must be {CARD_STANDARD}") content_class = compact_text(metadata.get("content_class")) if content_class and content_class not in CONTENT_CLASSES: errors.append( f"{path}: metadata.content_class must be one of " f"{', '.join(sorted(CONTENT_CLASSES))}" ) if content_class in {"authored-prose", "microcopy"} and ( not isinstance(depends_on, list) or "lov-branding-consistency" not in depends_on ): errors.append( f"{path}: {content_class} requires lov-branding-consistency" ) if content_class == "authored-prose": authorship_reference = path.parent / "references" / "authorship-integrity.md" if not authorship_reference.is_file(): errors.append( f"{authorship_reference}: authored-prose contract is required" ) if "references/authorship-integrity.md" not in body: errors.append( f"{path}: authored-prose must route to references/authorship-integrity.md" ) trigger_block = re.search( r"(?ms)^##\s+Triggers\s*$([\s\S]*?)(?=^##\s+|\Z)", body ) if not trigger_block: errors.append(f"{path}: add an explicit '## Triggers' section") else: block = trigger_block.group(1) if len(re.findall(r"(?m)^\s*-\s+\S", block)) < 3: errors.append(f"{path}: add two activation examples and one non-trigger") if not re.search(r"[\u3400-\u9fff]", block): errors.append(f"{path}: add a concrete Chinese trigger phrase") if not re.search(r"(?i)\b(?:the|a|an|create|build|help|publish|review|use)\b", block): errors.append(f"{path}: add a concrete English trigger phrase") if not re.search( r"(?mi)^###\s+(?:Do not activate when|Non-triggers?|不应触发|不要触发)\s*$", body, ): errors.append(f"{path}: add explicit non-trigger conditions") if len(read_text(path).splitlines()) >= 500: errors.append(f"{path}: keep SKILL.md below 500 lines") if not body.strip(): errors.append(f"{path}: body is empty") return data def load_yaml(path: Path, errors: list[str]) -> dict[str, Any] | None: try: data = yaml.safe_load(read_text(path)) except yaml.YAMLError as exc: errors.append(f"{path}: standard YAML parser rejected file: {exc}") return None if not isinstance(data, dict): errors.append(f"{path}: expected a YAML mapping") return None return data def validate_runtime_manifest( skill_root: Path, expected_skill_id: str, errors: list[str] ) -> None: manifest_path = skill_root / "skill.yaml" if not manifest_path.is_file(): errors.append(f"{manifest_path}: user-profile runtime manifest is required") return data = load_yaml(manifest_path, errors) if data is None: return if data.get("schema") != MANIFEST_SCHEMA: errors.append(f"{manifest_path}: schema must be {MANIFEST_SCHEMA}") if data.get("id") != expected_skill_id: errors.append(f"{manifest_path}: id must match {expected_skill_id}") version = compact_text(data.get("version")) if not SEMVER_RE.fullmatch(version): errors.append(f"{manifest_path}: version must use SemVer") if data.get("runtime") != RUNTIME_VERSION: errors.append(f"{manifest_path}: runtime must be {RUNTIME_VERSION}") context = data.get("context") if not isinstance(context, dict): errors.append(f"{manifest_path}: context must be a mapping") return profile = context.get("profile") preferences = context.get("preferences") interaction = context.get("interaction") if not isinstance(profile, dict): errors.append(f"{manifest_path}: context.profile must be a mapping") else: if profile.get("schema") != PROFILE_SCHEMA: errors.append(f"{manifest_path}: context.profile.schema must be {PROFILE_SCHEMA}") if profile.get("source") != "shared-profile": errors.append(f"{manifest_path}: context.profile.source must be shared-profile") reads = profile.get("read") if not isinstance(reads, list) or not reads or not all( isinstance(item, str) and item.strip() for item in reads ): errors.append(f"{manifest_path}: context.profile.read must be a non-empty list") persist = profile.get("persist") if not isinstance(persist, dict): errors.append(f"{manifest_path}: context.profile.persist must be a mapping") else: if persist.get("enabled") is not True: errors.append(f"{manifest_path}: profile persistence must be enabled") if persist.get("namespace") != f"skills.{expected_skill_id}": errors.append(f"{manifest_path}: persist.namespace must target skills.{expected_skill_id}") if persist.get("records_path") != f"skills.{expected_skill_id}.records": errors.append(f"{manifest_path}: persist.records_path must target Skill records") if persist.get("write_policy") != "direct-user-statement": errors.append(f"{manifest_path}: persist.write_policy must be direct-user-statement") if persist.get("atomic") is not True: errors.append(f"{manifest_path}: profile persistence must be atomic") fields = profile.get("fields") if not isinstance(fields, list) or not fields: errors.append(f"{manifest_path}: context.profile.fields must be a non-empty list") else: validate_manifest_fields(manifest_path, "profile", fields, errors) if not isinstance(preferences, dict): errors.append(f"{manifest_path}: context.preferences must be a mapping") else: fields = preferences.get("fields") if not isinstance(fields, list): errors.append(f"{manifest_path}: context.preferences.fields must be a list") else: validate_manifest_fields(manifest_path, "preferences", fields, errors) if not isinstance(interaction, dict): errors.append(f"{manifest_path}: context.interaction must be a mapping") else: if not isinstance(interaction.get("ask_missing"), bool): errors.append(f"{manifest_path}: interaction.ask_missing must be boolean") max_questions = interaction.get("max_questions") if not isinstance(max_questions, int) or isinstance(max_questions, bool) or not 1 <= max_questions <= 3: errors.append(f"{manifest_path}: interaction.max_questions must be 1-3") def validate_manifest_fields( manifest_path: Path, source_name: str, fields: list[Any], errors: list[str] ) -> None: for index, field in enumerate(fields): label = f"{manifest_path}: context.{source_name}.fields[{index}]" if not isinstance(field, dict): errors.append(f"{label} must be a mapping") continue if not compact_text(field.get("path")): errors.append(f"{label}.path is required") if not isinstance(field.get("required"), bool): errors.append(f"{label}.required must be boolean") if not compact_text(field.get("question")): errors.append(f"{label}.question is required") aliases = field.get("aliases", []) if isinstance(aliases, str): aliases = [aliases] if not isinstance(aliases, list) or not all( isinstance(alias, str) and alias.strip() for alias in aliases ): errors.append(f"{label}.aliases must be a list of paths") def has_content(value: Any) -> bool: if isinstance(value, str): return bool(value.strip()) if isinstance(value, list): return any(has_content(item) for item in value) if isinstance(value, dict): return any(has_content(item) for item in value.values()) return value is not None def contains_placeholder(value: Any) -> bool: if isinstance(value, str): return bool(re.search(r"\bTODO\b|\{[^}]+\}", value, re.I)) if isinstance(value, list): return any(contains_placeholder(item) for item in value) if isinstance(value, dict): return any(contains_placeholder(item) for item in value.values()) return False def validate_card_bundle(skill_root: Path, errors: list[str]) -> None: card_path = skill_root / "skill-card.yaml" card_doc_path = skill_root / "skill-card.md" cases_path = skill_root / "cases" / "cases.json" pricing_path = skill_root / "pricing-card.yaml" for path in (card_path, card_doc_path, cases_path, pricing_path): if not path.is_file(): errors.append(f"{path}: required Skill trust-bundle file is missing") card = load_yaml(card_path, errors) if card_path.is_file() else None if card is not None: if card.get("schema") != CARD_STANDARD: errors.append(f"{card_path}: schema must be {CARD_STANDARD}") required = ( "description", "owner", "license", "use_case", "deployment", "requirements", "risks", "references", "output", "version", "ethical_considerations", "dimensions", "pricing", "distribution", ) for key in required: if key not in card or not has_content(card.get(key)): errors.append(f"{card_path}: required field '{key}' is missing or empty") dimensions = card.get("dimensions") if not isinstance(dimensions, list) or len(dimensions) < 3: errors.append(f"{card_path}: dimensions must contain at least three named dimensions") else: ids: set[str] = set() for index, dimension in enumerate(dimensions): label = f"{card_path}: dimensions[{index}]" if not isinstance(dimension, dict): errors.append(f"{label}: expected a mapping") continue dimension_id = compact_text(dimension.get("id")) if not dimension_id or dimension_id in ids: errors.append(f"{label}: id is required and must be unique") ids.add(dimension_id) for key in ("label", "description", "evidence"): if not compact_text(dimension.get(key)): errors.append(f"{label}: '{key}' is required") risks = card.get("risks") if not isinstance(risks, list) or not risks: errors.append(f"{card_path}: risks must contain at least one risk and mitigation") else: for index, risk in enumerate(risks): if not isinstance(risk, dict) or not compact_text(risk.get("risk")) or not compact_text(risk.get("mitigation")): errors.append(f"{card_path}: risks[{index}] needs risk and mitigation") distribution = card.get("distribution") if not isinstance(distribution, dict) or not isinstance(distribution.get("paid"), list) or not isinstance(distribution.get("free"), list): errors.append(f"{card_path}: distribution must declare paid and free lists") if contains_placeholder(card): errors.append(f"{card_path}: replace unresolved TODO or template placeholders") if card_doc_path.is_file(): card_doc = read_text(card_doc_path) required_headings = ( "Description", "Owner", "License", "Use Case", "Deployment Geography", "Requirements", "Known Risks", "References", "Skill Output", "Skill Version", "Ethical Considerations", "User Cases", "Dimension Map", "Pricing Basis", "Distribution", ) for heading in required_headings: if not re.search(rf"(?mi)^#+\s+{re.escape(heading)}", card_doc): errors.append(f"{card_doc_path}: add the '{heading}' section") if re.search(r"\bTODO\b|\{[^}]+\}", card_doc, re.I): errors.append(f"{card_doc_path}: replace unresolved TODO or template placeholders") if cases_path.is_file(): try: cases = json.loads(read_text(cases_path)) except json.JSONDecodeError as exc: errors.append(f"{cases_path}: invalid JSON: {exc}") cases = [] if not isinstance(cases, list) or not cases: errors.append(f"{cases_path}: include at least one real user case") else: for index, case in enumerate(cases): label = f"{cases_path}: cases[{index}]" if not isinstance(case, dict): errors.append(f"{label}: expected a mapping") continue for key in ("title", "description", "input", "prompt", "output"): if not has_content(case.get(key)): errors.append(f"{label}: '{key}' is required") if contains_placeholder(case): errors.append(f"{label}: replace unresolved TODO or template placeholders") for image_key in ("cover", "gallery"): image_values = case.get(image_key, []) if isinstance(image_values, str): image_values = [image_values] if not isinstance(image_values, list): errors.append(f"{label}: '{image_key}' must be a string or list") continue for image in image_values: if not isinstance(image, str) or not image.strip(): errors.append(f"{label}: '{image_key}' contains an empty path") elif not re.match(r"^(?:https?:|/|data:)", image) and not (skill_root / image).is_file(): errors.append(f"{label}: case asset does not exist: {image}") pricing = load_yaml(pricing_path, errors) if pricing_path.is_file() else None if pricing is not None: if pricing.get("schema") != PRICING_CARD_SCHEMA: errors.append(f"{pricing_path}: schema must be {PRICING_CARD_SCHEMA}") for key in ("model", "currency", "list_price_cny", "basis", "boundary", "review_trigger", "confidence"): if key not in pricing or (key != "list_price_cny" and not has_content(pricing.get(key))): errors.append(f"{pricing_path}: required field '{key}' is missing or empty") if contains_placeholder(pricing): errors.append(f"{pricing_path}: replace unresolved TODO or template placeholders") def validate_composition_reference(skill_root: Path, errors: list[str]) -> None: path = skill_root / "references" / "skill-composition.md" if not path.is_file(): errors.append(f"{path}: required Skill group composition record is missing") return text = read_text(path) required_headings = ( "Nearby Skills Inspected", "Atomic Handoffs", "Overlap Decisions", "Composition Decision", ) for heading in required_headings: if not re.search(rf"(?mi)^#+\s+{re.escape(heading)}", text): errors.append(f"{path}: add the '{heading}' section") if re.search(r"\bTODO\b|\{[^}]+\}", text, re.I): errors.append(f"{path}: replace unresolved TODO or template placeholders") def validate_kit(root: Path, skill_names: set[str], errors: list[str]) -> None: manifest = root / "kit.yaml" if not manifest.exists(): return data = load_yaml(manifest, errors) if data is None: return modules = data.get("modules") if not isinstance(modules, list) or not modules: errors.append(f"{manifest}: modules must be a non-empty list") return module_ids: set[str] = set() for index, module in enumerate(modules): label = f"{manifest}: modules[{index}]" if not isinstance(module, dict): errors.append(f"{label}: expected a mapping") continue module_id = compact_text(module.get("id")) skill_name = compact_text(module.get("skill")) relative = compact_text(module.get("path")) if not module_id or module_id in module_ids: errors.append(f"{label}: id is required and must be unique") module_ids.add(module_id) module_path = (root / relative).resolve() if ( not relative or not is_relative_to(module_path, root.resolve()) or not (module_path / "SKILL.md").is_file() ): errors.append(f"{label}: missing module at '{relative}/SKILL.md'") if skill_name not in skill_names: errors.append(f"{label}: unresolved child skill '{skill_name}'") pipelines = data.get("pipelines") if not isinstance(pipelines, dict) or not pipelines: errors.append(f"{manifest}: pipelines must be a non-empty mapping") return for pipeline, sequence in pipelines.items(): if not isinstance(sequence, list) or not sequence: errors.append(f"{manifest}: pipeline '{pipeline}' must be a non-empty list") continue missing = [str(item) for item in sequence if item not in module_ids] if missing: errors.append( f"{manifest}: pipeline '{pipeline}' has unknown modules: " + ", ".join(missing) ) def validate_local_references(root: Path, errors: list[str]) -> None: for path in iter_files(root): if path.suffix.lower() != ".md": continue text = read_text(path) for raw in MARKDOWN_LINK_RE.findall(text): target = raw.strip().split(maxsplit=1)[0].strip("<>").split("#", 1)[0] if ( not target or re.match(r"^[a-z][a-z0-9+.-]*:", target, re.I) or any(token in target for token in ("TODO", "{", "}")) ): continue if not (path.parent / target).resolve().exists(): errors.append(f"{path}: broken local link '{target}'") skill_root = path.parent if path.name == "SKILL.md" else root for variable, target in SKILL_PATH_RE.findall(text): if "TODO" in target: continue base = skill_root if variable == "SKILL_DIR" else root resolved = (base / target.rstrip(".,;:)")).resolve() if not is_relative_to(resolved, root.resolve()) or not resolved.exists(): errors.append(f"{path}: missing required resource '${variable}/{target}'") def validate_hygiene(root: Path, errors: list[str]) -> None: private_path = re.compile(r"(?:/Users/[^/\s]+/|[A-Za-z]:\\\\Users\\\\[^\\\s]+\\\\)") for path in root.rglob("*"): if any(part in SKIP_DIRS for part in path.relative_to(root).parts): continue if path.name in JUNK_NAMES or path.suffix.lower() in JUNK_SUFFIXES: errors.append(f"{path}: generated/cache artifact must not ship") for path in iter_files(root): if path.suffix.lower() not in TEXT_SUFFIXES or path.name == "validate_skill.py": continue text = read_text(path) if private_path.search(text): errors.append(f"{path}: contains a private absolute user path") if path.name != "init_skill.py" and re.search(r"\bTODO\s*[::]", text): errors.append(f"{path}: unresolved TODO placeholder") for relative in ("workbuddy", "scripts/build_workbuddy.py"): if (root / relative).exists(): errors.append( f"{root / relative}: platform distribution artifacts belong to skill-publish" ) def validate_source(root: Path, errors: list[str]) -> None: root_skill = root / "SKILL.md" skill_files = [root_skill, *sorted((root / "skills").glob("*/SKILL.md"))] if not root_skill.is_file(): errors.append(f"{root_skill}: file is required") return parsed: list[tuple[Path, dict[str, Any]]] = [] for path in skill_files: data = validate_skill_file(path, errors) if data: parsed.append((path, data)) names = {compact_text(data.get("name")) for _, data in parsed} if len(names) != len(parsed): errors.append(f"{root}: every embedded Skill must have a unique name") for path, data in parsed: metadata = data.get("metadata") if isinstance(metadata, dict) and metadata.get("card_standard") == CARD_STANDARD: validate_card_bundle(path.parent, errors) validate_composition_reference(path.parent, errors) validate_runtime_manifest(path.parent, compact_text(data.get("name")), errors) validate_kit(root, names, errors) readme = root / "README.md" if not readme.is_file(): errors.append(f"{readme}: file is required") elif parsed: metadata = parsed[0][1].get("metadata") version = compact_text(metadata.get("version")) if isinstance(metadata, dict) else "" if version and f"version-{version}-" not in read_text(readme): errors.append(f"{readme}: version badge must match {version}") validate_hygiene(root, errors) validate_local_references(root, errors) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("path", type=Path, help="Local Skill source directory") args = parser.parse_args() root = args.path.expanduser().resolve() if not root.is_dir(): print(f"ERROR: directory does not exist: {root}", file=sys.stderr) return 2 errors: list[str] = [] validate_source(root, errors) if errors: print(f"FAILED: {len(errors)} issue(s)") for error in errors: print(f"- {error}") return 1 print(f"PASSED: source validation ({root})") return 0 if __name__ == "__main__": raise SystemExit(main())
-
-
CHANGELOG.md 285 B
# Changelog ## [1.0.1] - 2026-09-07 - 将历史 slash command 升级为通用 Skill,补全自然语言触发、显式输入、共享 Profile 与验收边界。 - 保留业务目的,修正宿主耦合和不安全的隐式动作;迁移案例与实际业务验收分别记录。 -
LICENSE 1 KB · in bundle
-
pricing-card.yaml 425 B
schema: lovstudio/pricing-card/v1 model: free_entry currency: CNY list_price_cny: 0 basis: 轻量公开工具能力;当前证据主要是源码迁移与安装验证,采用免费入口积累业务结果。 boundary: 交付 Skill 源码与使用合同,不含第三方服务费用、账户或人工代运营。 review_trigger: 十次真实业务使用或依赖接口与维护成本明显变化后复评。 confidence: low -
README.md 1018 B
# CSS 精修 · CSS Polish  清理冗余 CSS 并在现有 Tailwind 项目中重构样式。 ## 安装 ```bash npx skills add lov-better-css -g -y ``` ## 使用 - 清理冗余 CSS 并在现有 Tailwind 项目中重构样式。 - Refactor CSS and Tailwind styles. 支持自然语言调用,具体执行步骤与边界见 [SKILL.md](SKILL.md)。分析、预览和实际修改分开处理。 ## 配置与依赖 读取 [共享 Profile](references/user-profile.md);项目路径、输出目录与品牌来自当前请求或用户配置。 业务工具和目标系统依赖见工作流;公开文案使用 lov-branding-consistency 审校。 ## 质量与案例 ```bash python3 scripts/validate_skill.py . ``` [案例](cases/cases.json) 记录真实迁移输入和输出,仅证明所声明的验收范围。 [Skill Card](skill-card.md) 与 [定价依据](pricing-card.yaml) 说明使用边界;业务运行结果须逐次回读。 ## License MIT -
skill-card.md 1.1 KB
# CSS 精修 · CSS Polish · Skill Card ## Description 清理冗余 CSS 并在现有 Tailwind 项目中重构样式 ## Owner contributors ## License MIT ## Use Case 清理冗余 CSS 并在现有 Tailwind 项目中重构样式 ## Deployment Geography 本地运行;外部系统仅按用户明确目标访问。 ## Requirements 参见 SKILL.md 的兼容性和工作流。 ## Known Risks 项目版本与依赖不同;结构验收不等于业务运行成功。 ## References [工作流](SKILL.md) · [组合关系](references/skill-composition.md) ## Skill Output 清理冗余 CSS 并在现有 Tailwind 项目中重构样式 ## Skill Version 1.0.1 ## Ethical Considerations 不发布秘密,不伪造来源与验证结果。 ## User Cases [迁移案例](cases/cases.json);此案例仅覆盖通用化合同。 ## Dimension Map 可移植性、配置隔离、可追溯性;未给未经测量的业务评分。 ## Pricing Basis 免费入口;[价值与边界](pricing-card.yaml)。 ## Distribution GitHub 与 LovStudio:prepared;WorkBuddy 与 SkillPay:未选择。 -
skill-card.yaml 1.5 KB
schema: lovstudio/skill-card/v1 description: 清理冗余 CSS 并在现有 Tailwind 项目中重构样式 owner: contributors license: MIT use_case: 清理冗余 CSS 并在现有 Tailwind 项目中重构样式 deployment: 本地 Agent 宿主,业务系统按任务指定 requirements: 可读取文件的 Agent;业务依赖见 SKILL.md risks: - risk: 不同项目与工具版本存在差异,迁移验证不能替代业务运行验证。 mitigation: 执行前发现实际环境,按工作流回读结果。 references: - SKILL.md - cases/cases.json - references/skill-composition.md output: 清理冗余 CSS 并在现有 Tailwind 项目中重构样式 version: 1.0.1 ethical_considerations: 保留来源、用户授权和秘密边界,不虚构案例或外部完成状态。 dimensions: - id: portability label: 宿主可移植性 description: 显式输入与实际宿主能力 evidence: SKILL.md 的 Triggers 和 Execution boundary score: null - id: context label: 配置隔离 description: 用户配置与公开源分离 evidence: skill.yaml 与 Profile 合同 score: null - id: traceability label: 结果可追溯 description: 原始输入摘要与迁移结果 evidence: cases/cases.json;尚无业务效果评分 score: null pricing: model: free_entry basis: pricing-card.yaml distribution: free: - channel: github status: prepared - channel: lovstudio status: prepared paid: - channel: workbuddy status: not_selected - channel: skillpay status: not_selected -
SKILL.md 3.1 KB
--- name: lov-better-css description: 清理冗余 CSS 并在现有 Tailwind 项目中重构样式。支持明确输入与结果回读。Use to refactor CSS and Tailwind styles. license: MIT compatibility: 支持 Agent Skills 的宿主;Python 3.8+ 与 PyYAML 用于 Profile 和校验。业务工具要求见工作流。 depends_on: - lov-branding-consistency metadata: author: contributors version: 1.0.1 content_class: deterministic-output card_standard: lovstudio/skill-card/v1 tags: - better-css - portable-skill --- # CSS 精修 · CSS Polish 清理冗余 CSS 并在现有 Tailwind 项目中重构样式。 ## Triggers ### Activate when - The user asks to use this Skill for its documented outcome. - “清理冗余 CSS 并在现有 Tailwind 项目中重构样式。” - “Refactor CSS and Tailwind styles.” ### Do not activate when - 只是查询本 Skill 的说明,或请求与上述结果无关的任务;不执行实际业务操作。 - 用户仅要预览或审查时,不进入修改、提交或发布分支。 ## Execution boundary 自然语言请求即可触发;无需旧 slash 路径、参数插值或指定助手。明确解析当前请求中的 项目、目标文件、选项与输出位置;用当前宿主实际提供的文件、搜索、CLI 和浏览器能力。 项目依赖版本与外部 API 在执行时核实,不能假设示例是现行配置。随包脚本从 Skill 根解析, 业务文件从目标项目根解析。先读当前状态,保护已有未提交内容与其他任务的暂存区。 分析、预览请求保持只读;修改、提交、推送、部署和发布各依当前请求的明确范围执行。 不绕过保护、自动发送消息、强制结束用户进程或抢前台。失败保留可诊断原始错误。 ## Workflow 1. 读取指定样式与组件,记录动态 class、第三方约定、主题 token 和当前 Tailwind 版本。仅静态搜索无引用不能证明动态样式未使用。 2. 按重复、覆盖、未使用和可迁移样式建立修改清单;保留动画、伪元素、滚动条和必要覆盖规则。项目未采用 Tailwind 时不强行引入。 3. 逐组件合并样式并更新引用;遵循既有 semantic token,保持响应式、暗色、焦点与交互状态。用户要求 dry-run 时只交付差异方案。 4. 运行相关构建或类型检查;在可用页面对照关键断点和状态。报告实际体积变化,未测量时不填虚构百分比。 ## Composition 执行前读取 [能力组合](references/skill-composition.md),按明确制品交接相邻能力。 ## Runtime context (shared) 运行前读取本包 `skill.yaml` 与 [Profile 合同](references/user-profile.md)。优先级为当前请求、 项目上下文、本 Skill records、共享 preferences、brand/user Profile、安全默认值。 只读取声明字段;没有专用运行时的宿主可使用 `scripts/profile_store.py` 读取共享 Profile。 配置缺失只问影响结果的一个问题。用户明确要求长期保存的值通过该脚本原子写入, 报告实际路径;不保存推断、凭据或其他任务的资料。 -
skill.yaml 2.2 KB
schema: skill-manifest/v1 id: lov-better-css version: 1.0.1 runtime: skill-runtime/v1 context: profile: schema: user-profile/v1 source: shared-profile read: - user - brand - workspace - preferences - skills.lov-better-css persist: enabled: true namespace: skills.lov-better-css records_path: skills.lov-better-css.records write_policy: direct-user-statement atomic: true fields: - path: user.name aliases: - identity.name required: false question: 如果本次输出需要用户身份,请提供名称。 - path: user.language required: false question: 希望使用哪种语言输出? - path: user.timezone required: false question: 需要使用哪个时区处理日期和时间? - path: brand.name aliases: - identity.name required: false question: 如果本次输出需要品牌身份,请提供品牌名称。 - path: brand.site required: false question: 如果需要品牌官网,请提供地址。 - path: brand.tone required: false question: 如果已有品牌语气或审美关键词,请提供它们。 - path: identity.name required: false question: 如果本次输出需要品牌身份,请提供品牌名称。 - path: identity.logo required: false question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。 - path: workspace.project_root required: false question: 目标项目路径是什么? - path: workspace.output_dir required: false question: 输出保存在哪里? - path: workspace.knowledge_root required: false question: 本次知识库路径是什么? - path: skills.lov-better-css.profile required: false question: 本 Skill 有哪些明确配置? preferences: namespace: lov_better_css fields: - path: user.language required: false question: 希望使用哪种语言输出? - path: user.timezone required: false question: 需要使用哪个时区处理日期和时间? interaction: ask_missing: true max_questions: 1
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.