skill-normalize
把分散在多个 AI agent (Claude Code / Codex / CodeBuddy / WorkBuddy / agents-personal) 的重复 skill 合并到 ~/.skillhub/<slug>/ 单一权威拷贝,原 agent 路径替换为目录级 symlink。一处更新,所有 agent 生效。
> ℹ️ 本 Skill 是 skillhub CLI 的扩展子命令 normalize,由 scripts/skills_normalize.py 实现 + scripts/skillhub.sh 包装脚本驱动(安装后改名为 ~/.skillhub/skillhub,保持用户的 alias 习惯)。规范化只读模式默认输出,--apply 才动磁盘,所有改动 tar 全量备份后才落盘。
适用场景与边界
| 用户想做的事 | 是否触发本 Skill |
|---|---|
| 把 ~/.claude / ~/.codex / ~/.codebuddy 的同一个 skill 合并 | ✅ |
| 统一所有 AI agent 的 skill 数据源 | ✅ |
| 算一下 skill 重复占了多少空间 | ✅(用 --scan-only + --status) |
| 加新 agent (Cursor / Aider / Continue) 的扫描支持 | ✅(写 ~/.skillhub/.normalize-adapters.json,见 references/adapters.md) |
| 安装新 skill / 搜索 skillhub 商店 / 升级单个 skill | ❌(用 skillhub install / search / upgrade,不是本 Skill) |
| Linux / Windows 上跑 | ❌(本 Skill 仅 macOS 适配) |
工作流
1. 安装
skill 装到 ~/.skillhub/skill-normalize/ 之后,需要做一次性的 shell 集成。直接跑包内的安装脚本:
~/.skillhub/skill-normalize/scripts/install.sh
这一步会做:
1. 把 scripts/skills_normalize.py 软链到 ~/.skillhub/skills_normalize.py(与 skills_store_cli.py 同级)。
2. 把 scripts/skillhub.sh 软链到 ~/.skillhub/skillhub(去掉 .sh,保持用户 alias 习惯)。
3. 跑 python3 skills_normalize.py --bootstrap 把 normalize 子命令插进 skills_store_cli.py。
4. 给 shell rc 写 alias skillhub='~/.skillhub/skillhub'(可选;脚本会问)。
之后每次 CLI self-upgrade 时如果 skills_store_cli.py 被替换,包装脚本会在每次调用前自动重打补丁,对用户透明。
2. 探查(read-only)
skillhub normalize --scan-only # 列出每个 agent 装了多少 skill、多少已是 symlink
skillhub normalize --status # 列出已归一化的 skill + 反向链接数
skillhub normalize # 计算 plan 写到 ~/.skillhub/.normalize-plan.json,不动磁盘
3. 执行
skillhub normalize --apply --dry-run # 打印所有 mv / ln -s / cache invalidate 操作但不执行
skillhub normalize --apply # 真跑,备份在 ~/.skillhub/.normalize-backup/<ts>/
4. 回滚
skillhub normalize --rollback <ts> # 从备份还原;ts 用 ls ~/.skillhub/.normalize-backup/ 看
回滚做三件事:解 tar 还原 agent 原路径 → 删 ~/.skillhub/<slug>/ 权威副本 → 清 .skills_store_lock.json 里这次 apply 加的条目。
支持的 AI agents
| Adapter | 扫描路径 | 加载机制 |
|---|---|---|
claude |
~/.claude/skills/<slug>/SKILL.md |
fs scan |
claude-plugins |
~/.claude-internal/plugins/cache/<mp>/<plugin>/<ver>/skills/<slug>/SKILL.md |
installed_plugins.json 注册 |
codex |
~/.codex/vendor_imports/skills/skills/.curated/<slug>/SKILL.md |
skills-curated-cache.json |
codex-internal |
同 codex 但在 ~/.codex-internal/ |
同 codex |
codebuddy |
~/.codebuddy/plugins/marketplaces/*/{plugins,external_plugins}/*/skills/<slug>/SKILL.md |
marketplace cache |
workbuddy |
~/.workbuddy/connectors/skills/<conn>/SKILL/SKILL.md |
MCP connector |
agents-personal |
~/.agents/skills/<slug>/SKILL.md |
用户手动归一化目录 |
新 agent 通过 ~/.skillhub/.normalize-adapters.json 加,无需改代码。schema 见 references/adapters.md。
归一化判定(normalize-safe)
每个 skill 的 frontmatter 由 parse_frontmatter 取出后,按下表分类:
| Frontmatter 字段 | 处理 |
|---|---|
仅 name / description / shortDescription / icon* / repoPath / version / author / source |
✅ 归一化候选 |
含 allowed-tools(CodeBuddy 工具授权专属) |
❌ skipped,原样保留 + 写 reason |
含 connector(WorkBuddy MCP 连接器 ID 专属) |
❌ skipped |
含 disable(WorkBuddy 启用标志专属) |
❌ skipped |
把 CodeBuddy 的 allowed-tools skill 拷到 Codex 下不会出错,但工具授权会失效;把 WorkBuddy 的 connector skill 拷到 Claude 下没有 MCP server 它就废了。所以这类 skill 保留原样并标 reason,不归一化。
冲突处理
按 lowercase basename 分组后,组内 sha256:
- 全相等 → canonicalize action(合并)
- 不相等 → conflict action(同名不同内容,写到 plan 但 apply 跳过)
冲突常见来源:不同 agent vendor 了不同版本的同名 skill(比如 pdf 在 codex 和 codebuddy 里 hash 不一样)。看到 conflict 后用户可以:
1. 手动二选一然后删除其他副本
2. 跳过让两边各保留各的副本
3. 重命名其中一个让 hash 不同的也能共存
v1 不自动选——错的合并代价比保留重复大得多。
内置安全网
每次 --apply:
1. 单 skill 单事务:先 tar 全量备份目标 + 所有 appearance 路径到 ~/.skillhub/.normalize-backup/<ts>/,再 os.rename 权威源到 ~/.skillhub/<slug>/,再建 symlink。中途 crash → 备份完整可回滚。
2. 失效 cache:每个 adapter 声明的 cache 文件被 unlink,下次 agent 启动时重建,避免读到 stale state。
3. Symlink 形态保留:tar 不解引用 symlink,rollback 时 agent 原本是 symlink 还会还原成 symlink(早期版本曾把 symlink 还原成 regular dir,现已修复,回归测试覆盖)。
4. 已归一化跳过:所有 member 的 real_path 都指向 ~/.skillhub/<slug>/ 的组在 plan 阶段被过滤,所以 apply 是幂等的。
与 skillhub 现有功能的关系
| 子命令 | 谁负责 |
|---|---|
skillhub install <slug> |
不是本 Skill:从 metadata.json 配置的云端仓库下载 zip 装到 ~/.skillhub/<slug>/ |
skillhub search <query> |
不是本 Skill:调 skills_search_url 找云端 skill |
skillhub upgrade [<slug>] |
不是本 Skill:根据每个 skill config.json 里的 update URL 升级 |
skillhub list |
不是本 Skill:读 .skills_store_lock.json 列已装 |
skillhub normalize |
本 Skill:扫描本地各 AI agent 的 skill 目录、合并重复、symlink 归一 |
skillhub self-upgrade |
不是本 Skill:升级 CLI 自身 |
normalize 写到 lockfile 时 source 字段标 agent-import(区别于 skillhub 这个 install source),所以后续 list 和 upgrade 看得到这些条目,但不会去云端找它们的更新(云端没这些 slug,因为它们是从本地 agent 目录"逆向"来的)。
验证示例
跑完 skillhub normalize --apply 之后,验证三个独立 agent 路径下的 algorithmic-art 全部指向同一个权威拷贝:
for p in \
~/.claude-internal/plugins/cache/anthropic-agent-skills/document-skills/*/skills/algorithmic-art \
~/.claude-internal/plugins/cache/anthropic-agent-skills/example-skills/*/skills/algorithmic-art \
~/.claude-internal/plugins/cache/anthropic-agent-skills/claude-api/*/skills/algorithmic-art ; do
echo "$p -> $(readlink -f "$p")"
done
# 期望:3 行 readlink -f 输出全是 ~/.skillhub/algorithmic-art
架构与详细参考
references/architecture.md— 数据流图、operation 序列、单事务保证references/adapters.md— 自定义 adapter schema + 加新 agent 的步骤references/troubleshooting.md— 常见问题 + 调试命令