code-abyss 1.8.0 → 2.0.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **邪修红尘仙 · 宿命深渊**
6
6
 
7
- *一键为 Claude Code / Codex CLI 注入邪修人格与 56 篇安全工程秘典*
7
+ *一键为 Claude Code / Codex CLI 注入邪修人格与攻防安全工程知识体系*
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/code-abyss.svg)](https://www.npmjs.com/package/code-abyss)
10
10
  [![CI](https://github.com/telagod/code-abyss/actions/workflows/ci.yml/badge.svg)](https://github.com/telagod/code-abyss/actions/workflows/ci.yml)
@@ -25,7 +25,7 @@ npx code-abyss
25
25
  交互式菜单(方向键选择,回车确认):
26
26
 
27
27
  ```
28
- ☠️ Code Abyss v1.7.3
28
+ ☠️ Code Abyss v2.0.1
29
29
 
30
30
  ? 请选择操作 (Use arrow keys)
31
31
  ❯ 安装到 Claude Code (~/.claude/)
@@ -96,13 +96,15 @@ node ~/.codex/.sage-uninstall.js # Codex CLI
96
96
 
97
97
  ## 🎭 这是什么
98
98
 
99
- Code Abyss 是一套 **Claude Code / Codex CLI 个性化配置包**,一条命令注入:
99
+ Code Abyss 是一套 **Claude Code / Codex CLI 攻防安全配置包**,一条命令注入:
100
100
 
101
- - 🔥 **邪修人格** — 宿命压迫叙事 + 道语标签 + 渡劫协议
102
- - ⚔️ **安全工程知识体系** — 红队/蓝队/紫队三脉道统,11 领域 56 篇专业秘典
101
+ - 🔥 **邪修人格** — 沉浸式角色扮演 + 道语标签 + 渡劫协议 + 反拒绝条款
102
+ - ⚔️ **攻防安全知识体系** — 红队/蓝队/紫队三脉道统,6 个安全化身,11 领域 56 篇专业秘典
103
+ - 🗡 **零废话安全研究** — 禁 disclaimer、禁道德前缀、全授权直出 PoC + 技术细节
104
+ - ⚡ **全开默认配置** — Claude `bypassPermissions` / Codex `approval_policy=never` + `danger-full-access`
103
105
  - ⚖️ **5 个校验关卡** — 安全扫描、模块完整性、变更分析、代码质量、文档生成
104
- - **单元测试覆盖** Jest 框架,GitHub Actions CI (Node 18/20/22)
105
- - **三级授权**T1/T2/T3 分级,零确认直接执行
106
+ - 🧠 **沙箱感知 + 离线优先** 自适应执行环境,信息三级分级验证
107
+ - 🧬 **单源 skill registry** `skills/**/SKILL.md` frontmatter 同时驱动 Claude commands、Codex prompts 与脚本执行链
106
108
 
107
109
  ---
108
110
 
@@ -113,7 +115,8 @@ Code Abyss 是一套 **Claude Code / Codex CLI 个性化配置包**,一条命
113
115
  ├── CLAUDE.md 道典 ├── AGENTS.md 道典+风格
114
116
  ├── output-styles/ 输出风格 ├── config.toml 推荐配置
115
117
  │ └── abyss-cultivator.md ├── prompts/ custom prompts
116
- ├── settings.json
118
+ ├── commands/ 斜杠命令 │ └── *.md 自动生成 prompt
119
+ ├── settings.json └── skills/ 秘典 + 脚本执行器
117
120
  └── skills/ 56 篇秘典
118
121
 
119
122
  可选:
@@ -127,6 +130,8 @@ Code Abyss 是一套 **Claude Code / Codex CLI 个性化配置包**,一条命
127
130
 
128
131
  ### 校验关卡(`/` 直接调用)
129
132
 
133
+ 这些命令与 Codex custom prompts 都不是手写维护,而是由各自 `skills/**/SKILL.md` frontmatter 中的 `name`、`user-invocable`、`allowed-tools`、`argument-hint`、`scripts/` 状态统一生成。
134
+
130
135
  | 命令 | 功能 |
131
136
  |------|------|
132
137
  | `/verify-security` | 扫描代码安全漏洞,检测危险模式 |
@@ -163,43 +168,58 @@ Code Abyss 是一套 **Claude Code / Codex CLI 个性化配置包**,一条命
163
168
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
164
169
  "env": {
165
170
  "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
166
- "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
171
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
172
+ "CLAUDE_CODE_ENABLE_TASKS": "1",
173
+ "CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION": "1",
174
+ "ENABLE_TOOL_SEARCH": "auto:10"
167
175
  },
176
+ "defaultMode": "bypassPermissions",
168
177
  "alwaysThinkingEnabled": true,
178
+ "autoMemoryEnabled": true,
169
179
  "model": "opus",
170
180
  "outputStyle": "abyss-cultivator",
171
181
  "attribution": { "commit": "", "pr": "" },
182
+ "sandbox": { "autoAllowBashIfSandboxed": true },
172
183
  "permissions": {
173
- "allow": ["Bash", "LS", "Read", "Agent", "Write", "Edit", "MultiEdit",
174
- "Glob", "Grep", "WebFetch", "WebSearch", "TodoWrite",
175
- "NotebookRead", "NotebookEdit"]
184
+ "allow": ["Bash", "LS", "Read", "Edit", "Write", "MultiEdit",
185
+ "Agent", "Glob", "Grep", "WebFetch", "WebSearch",
186
+ "TodoWrite", "NotebookRead", "NotebookEdit", "mcp__*"]
176
187
  }
177
188
  }
178
189
  ```
179
190
 
180
191
  | 配置项 | 说明 |
181
192
  |--------|------|
193
+ | `defaultMode: bypassPermissions` | 跳过所有权限确认(`.git`等受保护目录仍会提示) |
194
+ | `autoMemoryEnabled` | 启用自动记忆,跨会话保留上下文 |
195
+ | `sandbox.autoAllowBashIfSandboxed` | 沙箱环境内自动放行 Bash 命令 |
182
196
  | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` | 启用多 Agent 并行协作(实验性) |
183
197
  | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | 禁用自动更新、遥测、错误报告 |
198
+ | `CLAUDE_CODE_ENABLE_TASKS` | 启用任务管理功能 |
199
+ | `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION` | 启用提示建议 |
200
+ | `ENABLE_TOOL_SEARCH` | MCP 工具自动搜索(auto:10 = 自动匹配前10个) |
201
+ | `mcp__*` | 自动放行所有 MCP 工具 |
184
202
  | `outputStyle` | 设置为 `abyss-cultivator` 启用邪修风格 |
185
203
 
186
204
  ---
187
205
 
188
206
  ### Codex `config.toml` 推荐模板
189
207
 
190
- 安装 `--target codex`(尤其 `-y`)时会写入以下 **safe 默认档** 到 `~/.codex/config.toml`:
208
+ 安装 `--target codex`(尤其 `-y`)时会写入以下 **全开默认档** 到 `~/.codex/config.toml`:
191
209
 
192
210
  ```toml
193
211
  model_provider = "custom"
194
212
  model = "gpt-5.2-codex"
195
213
  model_reasoning_effort = "high"
196
- approval_policy = "on-request"
197
- sandbox_mode = "workspace-write"
198
- disable_response_storage = true
199
-
200
- [profiles.full_access]
214
+ model_reasoning_summary = "detailed"
215
+ model_verbosity = "medium"
201
216
  approval_policy = "never"
202
217
  sandbox_mode = "danger-full-access"
218
+ disable_response_storage = true
219
+
220
+ [profiles.safe]
221
+ approval_policy = "on-request"
222
+ sandbox_mode = "workspace-write"
203
223
 
204
224
  [model_providers.custom]
205
225
  name = "custom"
@@ -212,18 +232,76 @@ web_search = true
212
232
 
213
233
  [features]
214
234
  multi_agent = true
235
+ shell_snapshot = true
236
+ undo = true
215
237
  ```
216
238
 
217
- - 日常交互默认使用 `on-request + workspace-write`,更贴近当前 Codex CLI 的低摩擦安全姿态
218
- - 需要高自动化时可显式切到 `full_access`:`codex -p full_access`
239
+ - 默认零审批 + 完全沙箱访问,适合安全研究/CTF/本地开发等高自动化场景
240
+ - `model_reasoning_summary = "detailed"` 输出详细推理摘要
241
+ - `shell_snapshot` / `undo` 启用快照与撤销功能
242
+ - 需要安全姿态时可显式切到 `safe`:`codex -p safe`
219
243
 
220
244
  ### 兼容性说明
221
245
 
222
246
  - 模板已对齐新版 Codex 配置风格:root keys、`[profiles.*]`、`[tools].web_search` 与 `[features].multi_agent`
247
+ - 默认档从 safe 切换为全开(`approval_policy = "never"` + `sandbox_mode = "danger-full-access"`),提供 `[profiles.safe]` 作为保守回退
248
+ - Claude Code 默认启用 `bypassPermissions` 模式,跳过所有权限确认(`.git` 等受保护目录仍会提示)
249
+ - 新增实验功能环境变量:`CLAUDE_CODE_ENABLE_TASKS`、`CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION`
250
+ - 新增 `mcp__*` 通配符,自动放行所有 MCP 工具
223
251
  - `Codex` 当前支持 `~/.codex/prompts/*.md` 作为 custom prompts;Code Abyss 会继续安装 `~/.codex/skills/`,并从 `user-invocable` skills 自动生成对应的 `prompts/`
252
+ - `Claude` 与 `Codex` 共用同一套 invocable skill 集合;只要 `user-invocable: true`,就会同步生成 `~/.claude/commands/*.md` 与 `~/.codex/prompts/*.md`
253
+ - `skills/run_skill.js` 现在仅负责执行脚本型 skill:通过共享 registry 定位脚本入口、加目标锁、spawn 子进程,并把退出码原样透传
254
+ - 若 skill 没有 `scripts/*.js`,Claude/Codex 两端都会退化为“先读 `SKILL.md`,再按秘典执行”的知识型模式
224
255
  - 安装器不会再为 Codex 写入伪配置 `~/.codex/settings.json`;若检测到旧版遗留文件,会在安装时备份后移除,卸载时恢复
225
- - 若你本地已有旧配置,安装器不会强制覆盖;会自动补齐 safe root 默认项、清理 removed feature、迁移 deprecated `web_search_*` 到 `[tools].web_search`,并仅在 `danger-full-access` 下清理 `projects.*.trust_level`
226
- - 建议升级后执行一次 `codex --help`,或用 `codex -p full_access --help` 校验 profile 可见性
256
+ - 若你本地已有旧配置,安装器不会强制覆盖;会自动补齐默认项、清理 removed feature、迁移 deprecated `web_search_*` 到 `[tools].web_search`
257
+ - 建议升级后执行一次 `codex --help`,或用 `codex -p safe --help` 校验 profile 可见性
258
+
259
+ ---
260
+
261
+ ## 🧩 Skill registry / 生成 / 执行链
262
+
263
+ 现在 `skills/**/SKILL.md` frontmatter 是唯一事实源,registry 会先把元数据标准化,再交给安装器与执行器消费。
264
+
265
+ ### 标准化 contract
266
+
267
+ 每个 skill 必须满足:
268
+
269
+ - 必填 frontmatter:`name`、`description`、`user-invocable`
270
+ - `name` 必须是 kebab-case slug,用作 `commands/*.md` / `prompts/*.md` 文件名
271
+ - `allowed-tools` 省略时默认 `Read`;若显式声明,则必须是 `Bash`、`Read`、`Write`、`Glob`、`Grep` 这类合法工具名列表
272
+ - `argument-hint` 可选,仅用于生成命令/提示词参数说明
273
+ - `category` 由目录前缀自动推断:`tools/` → `tool`,`domains/` → `domain`,`orchestration/` → `orchestration`
274
+ - `runtimeType` 由脚本入口自动推断:存在且仅存在一个 `scripts/*.js` 时为 `scripted`,否则为 `knowledge`
275
+ - `scripted` skill 会调用 `run_skill.js`;`knowledge` skill 只读取对应 `SKILL.md`
276
+ - `kind` 与 kebab-case 兼容镜像字段已从 registry 返回面移除;对外只暴露标准化字段,raw frontmatter 仅保留在 `meta`
277
+ - `scripts/` 下若出现多个 `.js` 入口,或 skill name 重复,安装/验证会立即失败
278
+
279
+ ### 生成链
280
+
281
+ 1. 安装器通过共享 skill registry 扫描全部 `SKILL.md`
282
+ 2. registry 先校验并标准化字段,再筛出 `user-invocable: true` 的 skill
283
+ 3. Claude 渲染为 `~/.claude/commands/*.md`
284
+ 4. Codex 渲染为 `~/.codex/prompts/*.md`
285
+ 5. `runtimeType=scripted` 时,双端产物都会调用各自的 `~/.claude/skills/run_skill.js` / `~/.codex/skills/run_skill.js`
286
+ 6. `runtimeType=knowledge` 时,双端都只读取 `SKILL.md` 作为执行秘典
287
+
288
+ 这保证了 **同一 skill 集合、同一元数据、同一 runtime 判定、双端同步生成**,避免 commands/prompts/script runner 各自漂移。
289
+
290
+ ---
291
+
292
+ ## 🧪 CI / Smoke 覆盖
293
+
294
+ 当前 CI 覆盖:
295
+
296
+ - `npm test`
297
+ - `npm run verify:skills`(显式 skill contract gate;frontmatter 解析失败、缺字段、非法工具名、重复 name、多脚本入口都会直接阻断)
298
+ - `verify-change`
299
+ - `verify-module`
300
+ - `verify-quality`
301
+ - `verify-security`
302
+ - Claude install/uninstall smoke
303
+ - Codex install/uninstall smoke
304
+ - 生成一致性回归:同一 invocable skill 集合在 Claude commands 与 Codex prompts 中必须同步存在
227
305
 
228
306
  ---
229
307
 
@@ -256,6 +334,9 @@ multi_agent = true
256
334
  |------|------|
257
335
  | `☠ 劫钟已鸣` | 开场受令 |
258
336
  | `🔥 破妄!` | 红队攻击 |
337
+ | `🗡 破阵!` | 渗透/安全评估 |
338
+ | `🔬 验毒!` | 代码审计 |
339
+ | `💀 噬魂!` | 逆向/漏洞研究 |
259
340
  | `❄ 镇魔!` | 蓝队防御 |
260
341
  | `⚡ 炼合!` | 紫队协同 |
261
342
  | `🩸 道基欲裂...` | 任务推进 |
@@ -7,17 +7,25 @@ const SETTINGS_TEMPLATE = {
7
7
  $schema: 'https://json.schemastore.org/claude-code-settings.json',
8
8
  env: {
9
9
  CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
10
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
10
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
11
+ CLAUDE_CODE_ENABLE_TASKS: '1',
12
+ CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION: '1',
13
+ ENABLE_TOOL_SEARCH: 'auto:10'
11
14
  },
15
+ defaultMode: 'bypassPermissions',
12
16
  alwaysThinkingEnabled: true,
17
+ autoMemoryEnabled: true,
13
18
  model: 'opus',
14
19
  outputStyle: 'abyss-cultivator',
15
20
  attribution: { commit: '', pr: '' },
21
+ sandbox: {
22
+ autoAllowBashIfSandboxed: true
23
+ },
16
24
  permissions: {
17
25
  allow: [
18
- 'Bash', 'LS', 'Read', 'Agent', 'Write', 'Edit', 'MultiEdit',
19
- 'Glob', 'Grep', 'WebFetch', 'WebSearch', 'TodoWrite',
20
- 'NotebookRead', 'NotebookEdit'
26
+ 'Bash', 'LS', 'Read', 'Edit', 'Write', 'MultiEdit',
27
+ 'Agent', 'Glob', 'Grep', 'WebFetch', 'WebSearch',
28
+ 'TodoWrite', 'NotebookRead', 'NotebookEdit', 'mcp__*'
21
29
  ]
22
30
  }
23
31
  };
@@ -4,8 +4,8 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
6
  const CODEX_DEFAULTS = {
7
- approvalPolicy: 'on-request',
8
- sandboxMode: 'workspace-write',
7
+ approvalPolicy: 'never',
8
+ sandboxMode: 'danger-full-access',
9
9
  featureFlag: 'multi_agent',
10
10
  };
11
11
 
package/bin/install.js CHANGED
@@ -15,8 +15,11 @@ if (parseInt(process.versions.node) < parseInt(MIN_NODE)) {
15
15
  process.exit(1);
16
16
  }
17
17
  const PKG_ROOT = fs.realpathSync(path.join(__dirname, '..'));
18
- const { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, parseFrontmatter } =
18
+ const { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog } =
19
19
  require(path.join(__dirname, 'lib', 'utils.js'));
20
+ const {
21
+ collectInvocableSkills,
22
+ } = require(path.join(__dirname, 'lib', 'skill-registry.js'));
20
23
  const { detectCclineBin, installCcline: _installCcline } = require(path.join(__dirname, 'lib', 'ccline.js'));
21
24
  const {
22
25
  detectCodexAuth: detectCodexAuthImpl,
@@ -151,39 +154,8 @@ function runUninstall(tgt) {
151
154
 
152
155
  // ── 安装核心 ──
153
156
 
154
- /**
155
- * 递归扫描 skills 目录,找出所有 user-invocable: true 的 SKILL.md
156
- * @param {string} skillsDir - skills 源目录绝对路径
157
- * @returns {Array<{meta: Object, relPath: string, hasScripts: boolean}>}
158
- */
159
157
  function scanInvocableSkills(skillsDir) {
160
- const results = [];
161
- function scan(dir) {
162
- const skillMd = path.join(dir, 'SKILL.md');
163
- if (fs.existsSync(skillMd)) {
164
- try {
165
- const content = fs.readFileSync(skillMd, 'utf8');
166
- const meta = parseFrontmatter(content);
167
- if (meta && meta['user-invocable'] === 'true' && meta.name) {
168
- const relPath = path.relative(skillsDir, dir);
169
- const scriptsDir = path.join(dir, 'scripts');
170
- const hasScripts = fs.existsSync(scriptsDir) &&
171
- fs.readdirSync(scriptsDir).some(f => f.endsWith('.js'));
172
- results.push({ meta, relPath, hasScripts });
173
- }
174
- } catch (e) { /* 解析失败跳过 */ }
175
- }
176
- try {
177
- fs.readdirSync(dir).forEach(sub => {
178
- const subPath = path.join(dir, sub);
179
- if (fs.statSync(subPath).isDirectory() && !shouldSkip(sub) && sub !== 'scripts') {
180
- scan(subPath);
181
- }
182
- });
183
- } catch (e) { /* 读取失败跳过 */ }
184
- }
185
- scan(skillsDir);
186
- return results;
158
+ return collectInvocableSkills(skillsDir);
187
159
  }
188
160
 
189
161
  const INVOCABLE_TARGETS = {
@@ -211,11 +183,13 @@ function getSkillPath(skillRoot, skillRelPath) {
211
183
  : `${skillRoot}/SKILL.md`;
212
184
  }
213
185
 
214
- function buildCommandFrontmatter(meta) {
215
- const desc = (meta.description || '').replace(/"/g, '\\"');
216
- const argHint = meta['argument-hint'];
217
- const tools = meta['allowed-tools'] || 'Read';
218
- const lines = ['---', `name: ${meta.name}`, `description: "${desc}"`];
186
+ function buildCommandFrontmatter(skill) {
187
+ const desc = (skill.description || '').replace(/"/g, '\\"');
188
+ const argHint = skill.argumentHint;
189
+ const tools = Array.isArray(skill.allowedTools)
190
+ ? skill.allowedTools.join(', ')
191
+ : (skill.allowedTools || 'Read');
192
+ const lines = ['---', `name: ${skill.name}`, `description: "${desc}"`];
219
193
 
220
194
  if (argHint) lines.push(`argument-hint: "${argHint}"`);
221
195
  lines.push(`allowed-tools: ${tools}`);
@@ -223,28 +197,48 @@ function buildCommandFrontmatter(meta) {
223
197
  return lines;
224
198
  }
225
199
 
226
- function buildClaudeBody(skillPath, meta, hasScripts) {
200
+ function buildSkillArtifactSpec(skill, targetName) {
201
+ const targetCfg = getInvocableTarget(targetName);
202
+ const runtimeType = skill.runtimeType || 'knowledge';
203
+ const allowedTools = Array.isArray(skill.allowedTools)
204
+ ? skill.allowedTools.join(', ')
205
+ : (skill.allowedTools || 'Read');
206
+ return {
207
+ targetName,
208
+ targetCfg,
209
+ name: skill.name,
210
+ description: skill.description,
211
+ argumentHint: skill.argumentHint || '',
212
+ allowedTools,
213
+ relPath: skill.relPath,
214
+ runtimeType,
215
+ scriptRunner: `node ${targetCfg.skillRoot}/run_skill.js ${skill.name} $ARGUMENTS`,
216
+ skillPath: getSkillPath(targetCfg.skillRoot, skill.relPath),
217
+ };
218
+ }
219
+
220
+ function buildClaudeBody(spec) {
227
221
  const lines = [];
228
- if (hasScripts) {
222
+ if (spec.runtimeType === 'scripted') {
229
223
  lines.push('以下所有步骤一气呵成,不要在步骤间停顿等待用户输入:', '');
230
- lines.push(`1. 读取规范:${skillPath}`);
231
- lines.push(`2. 执行命令:\`node ~/.claude/skills/run_skill.js ${meta.name} $ARGUMENTS\``);
224
+ lines.push(`1. 读取规范:${spec.skillPath}`);
225
+ lines.push(`2. 执行命令:\`${spec.scriptRunner}\``);
232
226
  lines.push('3. 按规范分析输出,完成后续动作', '');
233
227
  lines.push('全程不要停顿,不要询问是否继续。');
234
228
  return lines;
235
229
  }
236
230
 
237
231
  lines.push('读取以下秘典,根据内容为用户提供专业指导:', '');
238
- lines.push('```', skillPath, '```');
232
+ lines.push('```', spec.skillPath, '```');
239
233
  return lines;
240
234
  }
241
235
 
242
- function buildCodexPromptBody(skillPath, meta, hasScripts) {
236
+ function buildCodexPromptBody(spec) {
243
237
  const lines = [];
244
- if (meta['argument-hint']) lines.push(`Arguments: ${meta['argument-hint']}`, '');
245
- lines.push(`Read \`${skillPath}\` before acting.`, '');
246
- if (hasScripts) {
247
- lines.push(`Then run \`node ~/.codex/skills/run_skill.js ${meta.name} $ARGUMENTS\`.`);
238
+ if (spec.argumentHint) lines.push(`Arguments: ${spec.argumentHint}`, '');
239
+ lines.push(`Read \`${spec.skillPath}\` before acting.`, '');
240
+ if (spec.runtimeType === 'scripted') {
241
+ lines.push(`Then run \`${spec.scriptRunner}\`.`);
248
242
  lines.push('Do not stop between steps unless blocked by permissions or missing required inputs.');
249
243
  lines.push('Use the skill guidance plus script output to complete the task end-to-end.');
250
244
  return lines;
@@ -255,34 +249,44 @@ function buildCodexPromptBody(skillPath, meta, hasScripts) {
255
249
  return lines;
256
250
  }
257
251
 
258
- function generateInvocableContent(meta, skillRelPath, hasScripts, targetName) {
259
- const targetCfg = getInvocableTarget(targetName);
260
- const skillPath = getSkillPath(targetCfg.skillRoot, skillRelPath);
261
- const lines = targetName === 'claude' ? buildCommandFrontmatter(meta) : [];
252
+ function generateInvocableContent(skill, targetName) {
253
+ const spec = buildSkillArtifactSpec(skill, targetName);
254
+ const lines = targetName === 'claude' ? buildCommandFrontmatter(spec) : [];
262
255
  const body = targetName === 'claude'
263
- ? buildClaudeBody(skillPath, meta, hasScripts)
264
- : buildCodexPromptBody(skillPath, meta, hasScripts);
256
+ ? buildClaudeBody(spec)
257
+ : buildCodexPromptBody(spec);
265
258
  return [...lines, ...body, ''].join('\n');
266
259
  }
267
260
 
268
- function generateCommandContent(meta, skillRelPath, hasScripts) {
269
- return generateInvocableContent(meta, skillRelPath, hasScripts, 'claude');
261
+ function normalizeGeneratedSkill(meta, skillRelPath, runtimeType) {
262
+ return {
263
+ ...meta,
264
+ description: meta.description || '',
265
+ argumentHint: meta.argumentHint || '',
266
+ allowedTools: meta.allowedTools || 'Read',
267
+ relPath: skillRelPath,
268
+ runtimeType,
269
+ };
270
+ }
271
+
272
+ function generateCommandContent(meta, skillRelPath, runtimeType = 'knowledge') {
273
+ return generateInvocableContent(normalizeGeneratedSkill(meta, skillRelPath, runtimeType), 'claude');
270
274
  }
271
275
 
272
- function generatePromptContent(meta, skillRelPath, hasScripts) {
273
- return generateInvocableContent(meta, skillRelPath, hasScripts, 'codex');
276
+ function generatePromptContent(meta, skillRelPath, runtimeType = 'knowledge') {
277
+ return generateInvocableContent(normalizeGeneratedSkill(meta, skillRelPath, runtimeType), 'codex');
274
278
  }
275
279
 
276
280
  function installGeneratedArtifacts(skillsSrcDir, targetDir, backupDir, manifest, targetName) {
277
- const skills = scanInvocableSkills(skillsSrcDir);
281
+ const skills = collectInvocableSkills(skillsSrcDir);
278
282
  if (skills.length === 0) return 0;
279
283
 
280
284
  const targetCfg = getInvocableTarget(targetName);
281
285
  const installDir = path.join(targetDir, targetCfg.dir);
282
286
  fs.mkdirSync(installDir, { recursive: true });
283
287
 
284
- skills.forEach(({ meta, relPath, hasScripts }) => {
285
- const fileName = `${meta.name}.md`;
288
+ skills.forEach((skill) => {
289
+ const fileName = `${skill.name}.md`;
286
290
  const destFile = path.join(installDir, fileName);
287
291
  const relFile = path.posix.join(targetCfg.dir, fileName);
288
292
 
@@ -294,7 +298,7 @@ function installGeneratedArtifacts(skillsSrcDir, targetDir, backupDir, manifest,
294
298
  info(`备份: ${c.d(relFile)}`);
295
299
  }
296
300
 
297
- const content = generateInvocableContent(meta, relPath, hasScripts, targetName);
301
+ const content = generateInvocableContent(skill, targetName);
298
302
  fs.writeFileSync(destFile, content);
299
303
  manifest.installed.push(relFile);
300
304
  });
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { shouldSkip, parseFrontmatter } = require('./utils');
6
+
7
+ const DEFAULT_ALLOWED_TOOLS = ['Read'];
8
+ const NAME_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
+ const TOOL_NAME_RE = /^[A-Z][A-Za-z0-9]*$/;
10
+
11
+ function normalizeBoolean(value) {
12
+ return String(value).toLowerCase() === 'true';
13
+ }
14
+
15
+ function inferSkillKind(relPath) {
16
+ const normalizedRelPath = relPath.split(path.sep).join('/');
17
+ const [head] = normalizedRelPath.split('/');
18
+ if (head === 'tools') return 'tool';
19
+ if (head === 'domains') return 'domain';
20
+ if (head === 'orchestration') return 'orchestration';
21
+ return 'root';
22
+ }
23
+
24
+ function listScriptEntries(skillDir) {
25
+ const scriptsDir = path.join(skillDir, 'scripts');
26
+ let entries;
27
+ try {
28
+ entries = fs.readdirSync(scriptsDir)
29
+ .filter(name => name.endsWith('.js'))
30
+ .sort();
31
+ } catch {
32
+ return [];
33
+ }
34
+ return entries.map(name => path.join(scriptsDir, name));
35
+ }
36
+
37
+ function buildRegistryError(relPath, message) {
38
+ const where = relPath || '.';
39
+ return new Error(`无效 skill (${where}): ${message}`);
40
+ }
41
+
42
+ function requireStringField(meta, fieldName, relPath) {
43
+ const value = meta[fieldName];
44
+ if (typeof value !== 'string' || value.trim() === '') {
45
+ throw buildRegistryError(relPath, `缺少必填 frontmatter 字段 '${fieldName}'`);
46
+ }
47
+ return value.trim();
48
+ }
49
+
50
+ function normalizeAllowedTools(value, relPath) {
51
+ if (value === undefined || value === null || String(value).trim() === '') {
52
+ return [...DEFAULT_ALLOWED_TOOLS];
53
+ }
54
+
55
+ const tools = String(value)
56
+ .split(',')
57
+ .map(tool => tool.trim())
58
+ .filter(Boolean);
59
+
60
+ if (tools.length === 0) {
61
+ throw buildRegistryError(relPath, 'allowed-tools 不能为空');
62
+ }
63
+
64
+ for (const tool of tools) {
65
+ if (!TOOL_NAME_RE.test(tool)) {
66
+ throw buildRegistryError(relPath, `allowed-tools 包含非法值 '${tool}'`);
67
+ }
68
+ }
69
+
70
+ return tools;
71
+ }
72
+
73
+ function normalizeSkillRecord(skillsDir, skillDir, meta) {
74
+ const relPath = path.relative(skillsDir, skillDir);
75
+ const normalizedMeta = meta || {};
76
+ const scriptEntries = listScriptEntries(skillDir);
77
+
78
+ if (scriptEntries.length > 1) {
79
+ throw buildRegistryError(relPath, `scripts/ 目录下只能有一个 .js 入口,当前找到 ${scriptEntries.length} 个`);
80
+ }
81
+
82
+ const name = requireStringField(normalizedMeta, 'name', relPath);
83
+ if (!NAME_SLUG_RE.test(name)) {
84
+ throw buildRegistryError(relPath, `name 必须是 kebab-case slug,当前为 '${name}'`);
85
+ }
86
+
87
+ const description = requireStringField(normalizedMeta, 'description', relPath);
88
+ if (!Object.prototype.hasOwnProperty.call(normalizedMeta, 'user-invocable')) {
89
+ throw buildRegistryError(relPath, "缺少必填 frontmatter 字段 'user-invocable'");
90
+ }
91
+
92
+ const userInvocable = normalizeBoolean(normalizedMeta['user-invocable']);
93
+ const allowedTools = normalizeAllowedTools(normalizedMeta['allowed-tools'], relPath);
94
+ const argumentHint = normalizedMeta['argument-hint'] || '';
95
+ const category = inferSkillKind(relPath);
96
+ const runtimeType = scriptEntries.length === 1 ? 'scripted' : 'knowledge';
97
+ const scriptPath = scriptEntries[0] || null;
98
+ const skillPath = path.join(skillDir, 'SKILL.md');
99
+
100
+ return {
101
+ name,
102
+ description,
103
+ userInvocable,
104
+ allowedTools,
105
+ argumentHint,
106
+ relPath,
107
+ category,
108
+ runtimeType,
109
+ hasScripts: runtimeType === 'scripted',
110
+ scriptPath,
111
+ skillPath,
112
+ meta: normalizedMeta,
113
+ };
114
+ }
115
+
116
+ function validateUniqueSkillRecords(skills) {
117
+ const names = new Map();
118
+ const relPaths = new Map();
119
+
120
+ for (const skill of skills) {
121
+ if (names.has(skill.name)) {
122
+ throw buildRegistryError(skill.relPath, `重复的 skill name '${skill.name}',首次出现在 ${names.get(skill.name)}`);
123
+ }
124
+ names.set(skill.name, skill.relPath);
125
+
126
+ if (relPaths.has(skill.relPath)) {
127
+ throw buildRegistryError(skill.relPath, `重复的 skill relPath '${skill.relPath}'`);
128
+ }
129
+ relPaths.set(skill.relPath, skill.name);
130
+ }
131
+ }
132
+
133
+ function collectSkills(skillsDir) {
134
+ const results = [];
135
+
136
+ function scan(dir) {
137
+ const skillMd = path.join(dir, 'SKILL.md');
138
+ if (fs.existsSync(skillMd)) {
139
+ const relPath = path.relative(skillsDir, dir);
140
+ const content = fs.readFileSync(skillMd, 'utf8');
141
+ const meta = parseFrontmatter(content);
142
+ if (!meta) {
143
+ throw buildRegistryError(relPath, 'SKILL.md 缺少可解析的 frontmatter');
144
+ }
145
+ results.push(normalizeSkillRecord(skillsDir, dir, meta));
146
+ }
147
+
148
+ let entries;
149
+ try {
150
+ entries = fs.readdirSync(dir, { withFileTypes: true });
151
+ } catch {
152
+ return;
153
+ }
154
+
155
+ for (const entry of entries) {
156
+ if (!entry.isDirectory()) continue;
157
+ if (entry.name === 'scripts' || shouldSkip(entry.name)) continue;
158
+ scan(path.join(dir, entry.name));
159
+ }
160
+ }
161
+
162
+ scan(skillsDir);
163
+ validateUniqueSkillRecords(results);
164
+ return results.sort((a, b) => a.name.localeCompare(b.name));
165
+ }
166
+
167
+ function collectInvocableSkills(skillsDir) {
168
+ return collectSkills(skillsDir).filter(skill => skill.userInvocable);
169
+ }
170
+
171
+ function resolveSkillByName(skillsDir, skillName) {
172
+ return collectSkills(skillsDir).find(skill => skill.name === skillName) || null;
173
+ }
174
+
175
+ function resolveExecutableSkillScript(skillsDir, skillName) {
176
+ const skill = resolveSkillByName(skillsDir, skillName);
177
+ if (!skill) return { skill: null, scriptPath: null, reason: 'missing' };
178
+ if (skill.runtimeType !== 'scripted' || !skill.scriptPath) {
179
+ return { skill, scriptPath: null, reason: 'no-script' };
180
+ }
181
+ return { skill, scriptPath: skill.scriptPath, reason: null };
182
+ }
183
+
184
+ module.exports = {
185
+ collectSkills,
186
+ collectInvocableSkills,
187
+ resolveSkillByName,
188
+ resolveExecutableSkillScript,
189
+ inferSkillKind,
190
+ };
package/bin/lib/utils.js CHANGED
@@ -77,9 +77,15 @@ function parseFrontmatter(content) {
77
77
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
78
78
  if (!match) return null;
79
79
  const meta = Object.create(null);
80
- match[1].split('\n').forEach(line => {
81
- const m = line.match(/^([\w][\w-]*)\s*:\s*(.+)/);
82
- if (m && !UNSAFE_KEYS.has(m[1])) meta[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
80
+ match[1].split(/\r?\n/).forEach((rawLine, index) => {
81
+ const line = rawLine.trim();
82
+ if (!line || line.startsWith('#')) return;
83
+
84
+ const m = rawLine.match(/^([\w][\w-]*)\s*:\s*(.+)$/);
85
+ if (!m) {
86
+ throw new Error(`frontmatter 第 ${index + 1} 行格式无效: ${rawLine}`);
87
+ }
88
+ if (!UNSAFE_KEYS.has(m[1])) meta[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
83
89
  });
84
90
  return meta;
85
91
  }