@yottameta/yotta-compliance 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md ADDED
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: yotta-compliance
3
+ version: 0.1.1
4
+ description: 元规 —— 本地确定性的合规条款审查技能:对 UTF-8 文本 / Markdown 按版本化 JSON 规则包做确定性匹配、断言求值与证据抽取,输出带原文位置、命中规则、框架覆盖级别与来源条款的 Markdown / JSON 报告;当前 baseline_review 覆盖 PIPL 11 条规则与数据出境 4 条规则,GDPR / HIPAA / SOC2 / PCI-DSS / ISO27001 / 等保仅做 mapping_only 主题映射。触发:用户要审查隐私政策 / 数据处理条款 / 个人信息合规文本、检查 PIPL 或数据出境条款缺口、核对框架覆盖范围、生成可复算的合规审查报告、把条款审查接入 CI 闸门时。边界:只做基于确定性规则的审查建议,不构成法律意见、不替代律师或合规顾问;核心路径不调用 LLM、不联网、不解析 PDF / docx;mapping_only 框架不输出独立合规结论;不判断合同效力或诉讼结果。
5
+ license: MIT
6
+ ---
7
+
8
+ # 元规(yotta-compliance)
9
+
10
+ 本地、确定性、可复算的合规条款审查技能。元规对文本或 Markdown 执行规则匹配、断言求值与证据抽取,
11
+ 输出带**原文位置、命中规则、框架覆盖级别与来源条款**的审查报告。
12
+
13
+ 可信契约:
14
+
15
+ - 每条风险结论必须回到原文证据、规则 id 和来源条款;
16
+ - 没有证据不输出风险结论;
17
+ - 同一输入得到同一结果(仅生成时间不同);
18
+ - `mapping_only` 框架只做主题映射,不输出该框架的独立合规结论;
19
+ - 核心路径不调用 LLM、不联网,规则包只作为只读 JSON 数据。
20
+
21
+ ## 何时使用
22
+
23
+ - 审查隐私政策、数据处理条款、个人信息处理规则等文本;
24
+ - 检查文本是否出现 PIPL 要求的告知、合法性依据、保存期限、单独同意、个人权利、影响评估等要素;
25
+ - 检查数据出境场景是否出现安全评估 / 标准合同 / 保护认证、单独同意、境外接收方告知、境内存储说明;
26
+ - 生成可复算的 Markdown / JSON 审查报告,作为人工复核的起点;
27
+ - 把达到指定严重度的条款缺口接入 CI 闸门。
28
+
29
+ **Do NOT trigger**:
30
+
31
+ - 不生成法律意见、不判断合同效力、不预测诉讼结果;
32
+ - 不替代律师、合规顾问或监管机构的判断;
33
+ - 不把 `mapping_only` 描述为“已审查该框架”;
34
+ - v0.1 不直接解析 PDF / docx,需先转成 UTF-8 文本或 Markdown;
35
+ - 不联网检索法条,不调用模型做语义裁决;
36
+ - 不自动修改合同或替用户签署、提交任何文件。
37
+
38
+ ## 快速使用
39
+
40
+ Windows 使用 `python`,Linux / macOS 使用 `python3`。
41
+
42
+ ```bash
43
+ # 审查一个文本 / Markdown 文件(默认装载 pipl + data-export)
44
+ python3 scripts/yotta_compliance.py review --input contract.md
45
+
46
+ # 只审查 PIPL 包
47
+ python3 scripts/yotta_compliance.py review --input contract.md --frameworks pipl
48
+
49
+ # 输出 JSON,并在达到 high 时返回退出码 1
50
+ python3 scripts/yotta_compliance.py review --input contract.md --format json --gate high --out report.json
51
+
52
+ # 从标准输入读取
53
+ python3 scripts/yotta_compliance.py review --stdin --frameworks pipl,data-export
54
+
55
+ # 规则包操作
56
+ python3 scripts/yotta_compliance.py rules list --framework pipl
57
+ python3 scripts/yotta_compliance.py rules show PIPL-NOTICE-001
58
+ python3 scripts/yotta_compliance.py rules validate --pack rules/pipl.json
59
+ ```
60
+
61
+ 报告默认写 stdout。只在显式提供 `--out` 时写文件,输出路径不得与输入文件相同,也不得写入规则包目录。
62
+
63
+ ## 命令一览
64
+
65
+ | 命令 | 说明 |
66
+ |---|---|
67
+ | `review --input <file>` | 审查 UTF-8 `.txt` / `.md` / `.markdown` 文件 |
68
+ | `review --stdin` | 从标准输入读取文本 |
69
+ | `review --frameworks <list>` | 选择规则包,逗号分隔;缺省装载全部规则包 |
70
+ | `review --format md\|json` | 输出 Markdown(默认)或 JSON |
71
+ | `review --out <file>` | 写报告文件;缺省写 stdout |
72
+ | `review --gate <level>` | CI 闸门:`off` / `low` / `medium` / `high` / `critical` |
73
+ | `review --min-severity <level>` | 只输出达到该严重度的 finding;汇总计数仍保留全量 |
74
+ | `review --include-safe` | 在 JSON `summary.checked_rules` 与 Markdown 未覆盖范围中列出已检查未命中的规则 |
75
+ | `rules list` | 列出规则包与规则 |
76
+ | `rules show <rule_id>` | 查看单条规则的说明、建议、来源与测试用例 |
77
+ | `rules validate --pack <file>` | 校验规则包;不合规立即失败 |
78
+
79
+ ## 退出码
80
+
81
+ | 退出码 | 含义 |
82
+ |---|---|
83
+ | 0 | 审查完成,未超过 gate |
84
+ | 1 | 审查完成,存在达到 gate 的 finding |
85
+ | 2 | 输入缺失、编码错误、路径不可用或超出输入上限 |
86
+ | 3 | 规则包缺失、schema 无效或重复 `rule_id` |
87
+ | 4 | CLI 用法错误 |
88
+
89
+ ## 覆盖范围
90
+
91
+ 当前正式规则包:
92
+
93
+ | 规则包 | 框架 | 覆盖级别 | 规则数 |
94
+ |---|---|---|---|
95
+ | `pipl` | PIPL | `baseline_review` | 11 |
96
+ | `data-export` | 数据出境 | `baseline_review` | 4 |
97
+
98
+ `GDPR` / `HIPAA` / `SOC2` / `PCI-DSS` / `ISO27001` / 等保 当前为 `mapping_only`:
99
+ 只把基础规则映射到对应主题,不输出这些框架的独立合规结论。
100
+
101
+ 完整规则清单、主题映射与明确未覆盖范围见 `references/coverage.md`。
102
+
103
+ ## 报告包含什么
104
+
105
+ Markdown 报告固定包含七节:
106
+
107
+ 1. 输入摘要(来源、大小、行数、SHA-256、规则包版本);
108
+ 2. 框架覆盖摘要(规则包、框架、覆盖级别、状态、规则数);
109
+ 3. 风险汇总;
110
+ 4. 逐条发现(严重度、置信度、框架映射、原文证据、位置、规则说明、建议动作、规则来源);
111
+ 5. 未覆盖范围;
112
+ 6. 人工复核清单;
113
+ 7. 免责声明。
114
+
115
+ JSON 报告固定提供稳定机器契约:`schema_version` / `tool` / `tool_version` / `generated_at` /
116
+ `input` / `rule_packs` / `framework_coverage` / `summary` / `findings` / `review_items` / `disclaimer`。
117
+ 字段语义与证据结构见 `references/report-format.md`。
118
+
119
+ 示例(对一份缺少多项护栏的隐私说明运行默认规则包):
120
+
121
+ ```text
122
+ - 本次输出 11 条发现(全部 11 条)
123
+ - high:3 条
124
+ - medium:4 条
125
+ - low:4 条
126
+ - 已检查规则:15 条 | 命中规则:13 条
127
+ ```
128
+
129
+ 实际结果取决于输入内容与所选规则包,示例数字不作为固定输出。
130
+
131
+ ## 规则包与证据链
132
+
133
+ 规则包是版本化 JSON 数据,包含 pack 元信息、条款类型词表、规则列表和可选的主题映射。
134
+ 每条规则必须声明 `test_ids`,且正式规则在发布前必须配有正例、反例和边界例。
135
+
136
+ 每条 finding 固定包含:
137
+
138
+ - `finding_id`、`rule_id`、`severity`、`confidence`;
139
+ - `framework_mapping`(基础框架与 `框架:mapping_only` 标签);
140
+ - `evidence`(`matched_span` 带原文 quote 与起止行列;`document_scope` / `section_scope` 只给范围);
141
+ - `rationale`、`remediation`、`source`。
142
+
143
+ 规则编写、断言契约与验收要求见 `references/rule-authoring.md`。
144
+
145
+ ## 已知局限(必须人工复核)
146
+
147
+ - **词形匹配,不是语义理解**:absence 规则只判断检索词是否出现;同义但词形不同的表述可能漏检。
148
+ - **文档级范围**:absence 的 `scope` 均为 `document`,任何位置出现检索词即视为已出现,不判断该表述是否适用于当前处理活动。
149
+ - **否定句不建模**:「不会对外提供」「未经同意不得处理」等否定表述仍会触发候选匹配,可能产生漏报。
150
+ - **只解析阿拉伯数字**:`保存期限:N年` 可解析;中文数字(如“三年”)不解析、不换算。
151
+ - **数据出境人数阈值未进入 v1**:需要单位感知的数值原语;相关阈值规则推迟到规则包 v2。
152
+ - **absence 类结论一律降低置信度**:medium / low 置信 finding 会进入人工复核清单,必须人工核验后才能采信。
153
+ - **不直接解析 PDF / docx**:请先用文档适配器转成 UTF-8 文本或 Markdown。
154
+
155
+ ## 数据与安全边界
156
+
157
+ - 纯本地运行:不发送原文、证据或报告,不联网检索法条,不调用模型;
158
+ - 规则包是只读数据,不从规则正文执行代码;
159
+ - 输入上限 2 MiB;仅接受 UTF-8 文本;
160
+ - 除非显式设置 `--out`,报告只写 stdout;
161
+ - 输出路径不得与输入文件相同,也不得位于规则包目录内;
162
+ - 不缓存原文,不把合同内容写入日志。
163
+
164
+ ## 使用范围与授权
165
+
166
+ - 只审查用户有权处理的文本;用户对输入来源与使用范围负责;
167
+ - 审查结果用于发现文本缺口并辅助人工复核,不构成法律意见;
168
+ - 不用于规避监管、伪造合规结论或对外声称“已通过某框架认证”;
169
+ - 引用规则来源时保留规则包中的条款号与链接,不凭记忆补写条款。
170
+
171
+ ## 法律与红线
172
+
173
+ - 元规是**审查建议工具**,不是律师事务所、认证机构或监管机关;
174
+ - 规则命中只代表文本中出现了可观察现象,不代表真实业务未履行义务;
175
+ - 规则未命中不代表不存在风险;`mapping_only` 框架不得被解读为已完成合规审查;
176
+ - 使用本工具须遵守所在地法律法规与平台条款,最终判断由具备资质的专业人士作出。
177
+
178
+ ## 家族协同
179
+
180
+ - **元信 yotta-verify**:规则包与技能发布前的确定性安全扫描;
181
+ - **元忆 yotta-memory**:仅在用户显式选择时保存报告摘要与规则包哈希,不自动保存合同原文;
182
+ - **元呈 yotta-present**:把审查结论呈现为报告或证据卡;
183
+ - **元安 yotta-security-audit**:审计规则包与文件读取面的安全风险;
184
+ - **元审 yotta-vetter**:自定义规则包安装前的来源与权限审查。
185
+
186
+ ## 开发与校验
187
+
188
+ ```bash
189
+ # 全量契约测试(技能目录内)
190
+ python3 scripts/test_yotta_compliance.py
191
+
192
+ # 规则包校验
193
+ python3 scripts/yotta_compliance.py rules validate --pack rules/pipl.json
194
+ python3 scripts/yotta_compliance.py rules validate --pack rules/data-export.json
195
+
196
+ # 语法检查
197
+ python3 -m py_compile scripts/yotta_compliance.py
198
+ ```
199
+
200
+ ## 参考文档
201
+
202
+ - `references/rule-authoring.md` — 规则包 schema、匹配原语、断言契约与规则验收;
203
+ - `references/report-format.md` — Markdown / JSON 报告契约、字段语义与退出码;
204
+ - `references/coverage.md` — 当前框架覆盖、规则清单、主题映射与明确未覆盖范围。
205
+
206
+ ## 免责声明
207
+
208
+ 本工具提供基于确定性规则的条款审查建议与证据链,不构成法律意见。
Binary file
package/bin/install.js ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * yotta-compliance 跨平台安装器(YottaSkills)
4
+ *
5
+ * 用法:
6
+ * npx -y @yottameta/yotta-compliance --agent <name> # 按智能体默认用户级目录安装(推荐)
7
+ * npx -y @yottameta/yotta-compliance --dir <path> # 装到指定目录(用户改了目录的智能体)
8
+ * npx -y @yottameta/yotta-compliance -g # 安装到全部已知智能体用户级目录
9
+ * npx -y @yottameta/yotta-compliance # 安装到检测到的项目级目录
10
+ * npx -y @yottameta/yotta-compliance --list # 列出智能体 -> 默认目录
11
+ * npx -y @yottameta/yotta-compliance --dry-run # 只显示将写入的目标,不写文件
12
+ * npx -y @yottameta/yotta-compliance --version # 显示版本
13
+ * npx -y @yottameta/yotta-compliance --help # 显示帮助
14
+ *
15
+ * 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
16
+ */
17
+ 'use strict';
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+
22
+ const SKILL_NAME = 'yotta-compliance';
23
+ const PKG_ROOT = path.join(__dirname, '..');
24
+
25
+ // 顶层开发文件 / 目录:只在安装包顶层跳过;技能包内嵌套同名载荷必须保留。
26
+ // 与 install.sh 同口径:开发件与运行时不相关目录一律不落目标。
27
+ const TOP_SKIP = [
28
+ 'package.json', 'package-lock.json', 'bin', 'lib', 'test',
29
+ '.github', '.git', '.gitignore', '.npmignore', '.gitattributes',
30
+ '.yotta', '.tmp', 'install.sh', 'node_modules',
31
+ ];
32
+ const TOP_SKIP_SET = new Set(TOP_SKIP);
33
+ // 缓存 / 编译产物:任意层级跳过,安装时同样清理。
34
+ const CACHE_DIRS = new Set(['__pycache__', '.pytest_cache', '.mypy_cache']);
35
+
36
+ class UsageError extends Error {}
37
+ class TargetError extends Error {}
38
+ class InstallError extends Error {}
39
+
40
+ // 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
41
+ // 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
42
+ // Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
43
+ const AGENT_DIRS = {
44
+ claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
45
+ cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
46
+ codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
47
+ gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
48
+ goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
49
+ amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
50
+ opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
51
+ windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
52
+ workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
53
+ kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
54
+ trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
55
+ 'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
56
+ qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
57
+ comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
58
+ codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
59
+ kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
60
+ agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
61
+ };
62
+
63
+ const PROJECT_DIRS = [
64
+ '.claude/skills',
65
+ '.cursor/skills',
66
+ '.codex/skills',
67
+ '.config/goose/skills',
68
+ '.config/agents/skills',
69
+ '.opencode/skills',
70
+ '.codeium/windsurf/skills',
71
+ '.workbuddy/skills',
72
+ '.kiro/skills',
73
+ '.traecli/skills',
74
+ '.gemini/skills',
75
+ '.trae-cn/skills',
76
+ '.qwen/skills',
77
+ '.comate/skills',
78
+ '.codebuddy/skills',
79
+ '.kimi/skills',
80
+ '.agents/skills',
81
+ ];
82
+
83
+ // Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
84
+ function codexUserDir() {
85
+ const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
86
+ return path.join(base, 'skills');
87
+ }
88
+
89
+ // OpenCode 用户级目录特判:优先 $XDG_CONFIG_HOME/opencode/skills,否则 ~/.config/opencode/skills
90
+ function opencodeUserDir() {
91
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
92
+ return path.join(base, 'opencode', 'skills');
93
+ }
94
+
95
+ function resolveUserDir(rel) {
96
+ if (rel === '.codex/skills') return codexUserDir();
97
+ if (rel === '.config/opencode/skills') return opencodeUserDir();
98
+ return path.join(os.homedir(), rel);
99
+ }
100
+
101
+ function displayDir(rel) {
102
+ if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
103
+ return '~/' + rel;
104
+ }
105
+
106
+ function skillVersion() {
107
+ try {
108
+ const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
109
+ return data.version || null;
110
+ } catch (_) {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function usage() {
116
+ console.log(SKILL_NAME + ' 安装器(YottaSkills)');
117
+ console.log('');
118
+ console.log('用法:');
119
+ console.log(' node bin/install.js --agent <name> 按智能体默认用户级目录安装(推荐)');
120
+ console.log(' node bin/install.js --dir <path> 装到指定目录(用户改了目录的智能体)');
121
+ console.log(' node bin/install.js -g 安装到全部已知智能体用户级目录');
122
+ console.log(' node bin/install.js 安装到检测到的项目级目录');
123
+ console.log('');
124
+ console.log('参数:');
125
+ console.log(' --agent <name> 智能体键名,见 --list');
126
+ console.log(' --dir <path> 自定义技能目录');
127
+ console.log(' -g, --global 安装到全部已知用户级目录');
128
+ console.log(' --list, -l 列出支持的智能体目录');
129
+ console.log(' --dry-run 只显示将写入的目标,不写文件');
130
+ console.log(' --version, -v 显示版本');
131
+ console.log(' --help, -h 显示帮助');
132
+ console.log(' --yes, -y 兼容参数(-g 不再强制要求)');
133
+ console.log('');
134
+ console.log('退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误');
135
+ }
136
+
137
+ function parseArgs(argv) {
138
+ const opts = { help: false, version: false, list: false, global: false, dryRun: false, yes: false, dir: null, agent: null };
139
+ for (let i = 0; i < argv.length; i++) {
140
+ const arg = argv[i];
141
+ if (arg === '--help' || arg === '-h') opts.help = true;
142
+ else if (arg === '--version' || arg === '-v') opts.version = true;
143
+ else if (arg === '--list' || arg === '-l') opts.list = true;
144
+ else if (arg === '--global' || arg === '-g') opts.global = true;
145
+ else if (arg === '--dry-run') opts.dryRun = true;
146
+ else if (arg === '--yes' || arg === '-y') opts.yes = true;
147
+ else if (arg === '--dir') {
148
+ const value = argv[++i];
149
+ if (!value) throw new UsageError('--dir 需要一个非空路径');
150
+ opts.dir = value;
151
+ } else if (arg === '--agent') {
152
+ const value = argv[++i];
153
+ if (!value) throw new UsageError('--agent 需要一个非空名称');
154
+ opts.agent = String(value).toLowerCase();
155
+ } else {
156
+ throw new UsageError('未知参数: ' + arg);
157
+ }
158
+ }
159
+ if (!opts.help && !opts.version) {
160
+ const selected = [opts.dir, opts.agent, opts.global].filter(Boolean).length;
161
+ if (selected > 1) throw new UsageError('--dir / --agent / -g 只能选一个');
162
+ }
163
+ return opts;
164
+ }
165
+
166
+ function printList() {
167
+ console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
168
+ for (const [key, value] of Object.entries(AGENT_DIRS)) {
169
+ const resolved = value.dirs.map(displayDir);
170
+ console.log(' ' + key.padEnd(10) + value.label.padEnd(18) + resolved.join('、'));
171
+ }
172
+ console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
173
+ console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
174
+ }
175
+
176
+ function assertSafeTarget(target) {
177
+ const rel = path.relative(PKG_ROOT, target);
178
+ if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
179
+ throw new UsageError('目标目录不能在技能源目录内(防止自装自毁)');
180
+ }
181
+ }
182
+
183
+ function shouldSkipCache(name, isFile) {
184
+ if (CACHE_DIRS.has(name)) return true;
185
+ if (isFile && (name.endsWith('.pyc') || name.endsWith('.pyo'))) return true;
186
+ return false;
187
+ }
188
+
189
+ function copyDir(src, dst, topLevel) {
190
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
191
+ // 只跳过安装包顶层开发文件;template/ 等嵌套同名载荷必须保留。
192
+ if (topLevel && TOP_SKIP_SET.has(entry.name)) continue;
193
+ if (shouldSkipCache(entry.name, entry.isFile())) continue;
194
+ const s = path.join(src, entry.name);
195
+ const d = path.join(dst, entry.name);
196
+ if (entry.isDirectory()) {
197
+ fs.mkdirSync(d, { recursive: true });
198
+ copyDir(s, d, false);
199
+ } else if (entry.isFile()) {
200
+ fs.copyFileSync(s, d);
201
+ }
202
+ }
203
+ }
204
+
205
+ // 清理旧版安装残留(fail-closed 白名单):
206
+ // 仅在目标目录已存在且含 SKILL.md 时触发;只删顶层开发项 + 任意层级缓存;
207
+ // 不整目录删除、不跟随符号链接。
208
+ function cleanResidue(target) {
209
+ const removed = [];
210
+ let stat = null;
211
+ try { stat = fs.statSync(target); } catch (_) { return removed; }
212
+ if (!stat.isDirectory()) return removed;
213
+ if (!fs.existsSync(path.join(target, 'SKILL.md'))) return removed;
214
+ for (const name of TOP_SKIP) {
215
+ const p = path.join(target, name);
216
+ let entry = null;
217
+ try { entry = fs.lstatSync(p); } catch (_) { continue; }
218
+ if (entry.isSymbolicLink()) continue;
219
+ fs.rmSync(p, { recursive: true, force: true });
220
+ removed.push(name);
221
+ }
222
+ const walk = (dir) => {
223
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
224
+ const p = path.join(dir, entry.name);
225
+ if (entry.isSymbolicLink()) continue;
226
+ if (entry.isDirectory()) {
227
+ if (CACHE_DIRS.has(entry.name)) {
228
+ fs.rmSync(p, { recursive: true, force: true });
229
+ removed.push(path.relative(target, p));
230
+ } else {
231
+ walk(p);
232
+ }
233
+ } else if (entry.isFile() && (entry.name.endsWith('.pyc') || entry.name.endsWith('.pyo'))) {
234
+ fs.unlinkSync(p);
235
+ removed.push(path.relative(target, p));
236
+ }
237
+ }
238
+ };
239
+ walk(target);
240
+ return removed;
241
+ }
242
+
243
+ function installTo(dest, opts) {
244
+ if (!dest || typeof dest !== 'string') throw new UsageError('目标目录不能为空');
245
+ const target = path.resolve(dest, SKILL_NAME);
246
+ assertSafeTarget(target);
247
+ if (opts.dryRun) {
248
+ console.log('[dry-run] 将安装到 -> ' + target);
249
+ return target;
250
+ }
251
+ try {
252
+ for (const rel of cleanResidue(target)) console.log('已清理残留: ' + rel);
253
+ fs.mkdirSync(target, { recursive: true });
254
+ copyDir(PKG_ROOT, target, true);
255
+ if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
256
+ throw new InstallError('安装结果缺少 SKILL.md');
257
+ }
258
+ } catch (err) {
259
+ if (err instanceof UsageError || err instanceof TargetError || err instanceof InstallError) throw err;
260
+ throw new InstallError('安装到 ' + target + ' 失败: ' + err.message);
261
+ }
262
+ console.log('installed -> ' + target);
263
+ return target;
264
+ }
265
+
266
+ function run() {
267
+ const opts = parseArgs(process.argv.slice(2));
268
+ if (opts.help) { usage(); return; }
269
+ if (opts.version) {
270
+ const version = skillVersion();
271
+ if (!version) {
272
+ console.error(SKILL_NAME + ' 版本未知(未找到 package.json)');
273
+ process.exitCode = 1;
274
+ return;
275
+ }
276
+ console.log(SKILL_NAME + ' v' + version);
277
+ return;
278
+ }
279
+ if (opts.list) { printList(); return; }
280
+
281
+ if (opts.dir) { installTo(opts.dir, opts); return; }
282
+
283
+ if (opts.agent) {
284
+ const info = AGENT_DIRS[opts.agent];
285
+ if (!info) {
286
+ throw new UsageError('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';自定义目录请用 --dir <路径>。');
287
+ }
288
+ installTo(resolveUserDir(info.dirs[0]), opts);
289
+ console.log('完成。');
290
+ return;
291
+ }
292
+
293
+ if (opts.global) {
294
+ const seen = new Set();
295
+ for (const value of Object.values(AGENT_DIRS)) {
296
+ for (const rel of value.dirs) {
297
+ if (seen.has(rel)) continue;
298
+ seen.add(rel);
299
+ installTo(resolveUserDir(rel), opts);
300
+ }
301
+ }
302
+ if (!opts.dryRun) console.log('完成。');
303
+ return;
304
+ }
305
+
306
+ const dirs = PROJECT_DIRS.filter((d) => fs.existsSync(d));
307
+ if (!dirs.length) {
308
+ throw new TargetError('未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。');
309
+ }
310
+ for (const dir of dirs) installTo(dir, opts);
311
+ if (!opts.dryRun) console.log('完成。');
312
+ }
313
+
314
+ function main() {
315
+ try {
316
+ run();
317
+ } catch (err) {
318
+ if (err instanceof UsageError) {
319
+ console.error('用法错误: ' + err.message);
320
+ usage();
321
+ process.exitCode = 2;
322
+ } else if (err instanceof TargetError) {
323
+ console.error('目标错误: ' + err.message);
324
+ process.exitCode = 4;
325
+ } else if (err instanceof InstallError) {
326
+ console.error('安装失败: ' + err.message);
327
+ console.error('修复建议: 检查目录权限与磁盘空间后重试,或用 --dir 换一个目录。');
328
+ process.exitCode = 1;
329
+ } else {
330
+ console.error('未知错误: ' + (err && err.message ? err.message : String(err)));
331
+ process.exitCode = 1;
332
+ }
333
+ }
334
+ }
335
+
336
+ main();