code-abyss 2.0.0 → 2.0.2

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.
@@ -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
+ };
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SUPPORTED_TARGETS = new Set(['claude', 'codex']);
7
+
8
+ function loadStyleRegistry(projectRoot) {
9
+ const registryPath = path.join(projectRoot, 'output-styles', 'index.json');
10
+ const raw = fs.readFileSync(registryPath, 'utf8');
11
+ const parsed = JSON.parse(raw);
12
+ const styles = Array.isArray(parsed.styles) ? parsed.styles : null;
13
+ if (!styles || styles.length === 0) {
14
+ throw new Error('output-styles/index.json 缺少 styles 列表');
15
+ }
16
+
17
+ const seen = new Set();
18
+ let defaultCount = 0;
19
+ const normalized = styles.map((style) => normalizeStyle(style, seen));
20
+ normalized.forEach((style) => {
21
+ if (style.default) defaultCount += 1;
22
+ });
23
+
24
+ if (defaultCount !== 1) {
25
+ throw new Error('style registry 必须且只能有一个 default style');
26
+ }
27
+
28
+ return normalized;
29
+ }
30
+
31
+ function normalizeStyle(style, seen) {
32
+ if (!style || typeof style !== 'object') {
33
+ throw new Error('style registry 存在无效条目');
34
+ }
35
+
36
+ const slug = requireNonEmptyString(style.slug, 'slug');
37
+ if (!/^[a-z0-9-]+$/.test(slug)) {
38
+ throw new Error(`style slug 必须是 kebab-case: ${slug}`);
39
+ }
40
+ if (seen.has(slug)) {
41
+ throw new Error(`style slug 重复: ${slug}`);
42
+ }
43
+ seen.add(slug);
44
+
45
+ const label = requireNonEmptyString(style.label, `styles.${slug}.label`);
46
+ const description = requireNonEmptyString(style.description, `styles.${slug}.description`);
47
+ const file = requireNonEmptyString(style.file || `${slug}.md`, `styles.${slug}.file`);
48
+ const targets = normalizeTargets(style.targets, slug);
49
+
50
+ return {
51
+ slug,
52
+ label,
53
+ description,
54
+ file,
55
+ targets,
56
+ default: style.default === true,
57
+ };
58
+ }
59
+
60
+ function requireNonEmptyString(value, fieldName) {
61
+ if (typeof value !== 'string' || value.trim() === '') {
62
+ throw new Error(`style registry 字段无效: ${fieldName}`);
63
+ }
64
+ return value.trim();
65
+ }
66
+
67
+ function normalizeTargets(targets, slug) {
68
+ const values = Array.isArray(targets) && targets.length > 0 ? targets : ['claude', 'codex'];
69
+ values.forEach((target) => {
70
+ if (!SUPPORTED_TARGETS.has(target)) {
71
+ throw new Error(`style ${slug} 包含不支持的 target: ${target}`);
72
+ }
73
+ });
74
+ return [...new Set(values)];
75
+ }
76
+
77
+ function listStyles(projectRoot, targetName = null) {
78
+ const styles = loadStyleRegistry(projectRoot);
79
+ if (!targetName) return styles;
80
+ return styles.filter(style => style.targets.includes(targetName));
81
+ }
82
+
83
+ function getDefaultStyle(projectRoot, targetName = null) {
84
+ const styles = listStyles(projectRoot, targetName);
85
+ const style = styles.find(item => item.default);
86
+ if (!style) {
87
+ throw new Error(`未找到 ${targetName || '全局'} 默认输出风格`);
88
+ }
89
+ return style;
90
+ }
91
+
92
+ function resolveStyle(projectRoot, slug, targetName = null) {
93
+ const styles = listStyles(projectRoot, targetName);
94
+ return styles.find(style => style.slug === slug) || null;
95
+ }
96
+
97
+ function readStyleContent(projectRoot, style) {
98
+ const stylePath = path.join(projectRoot, 'output-styles', style.file);
99
+ return fs.readFileSync(stylePath, 'utf8');
100
+ }
101
+
102
+ function renderCodexAgents(projectRoot, styleSlug) {
103
+ const style = resolveStyle(projectRoot, styleSlug, 'codex');
104
+ if (!style) {
105
+ throw new Error(`未知输出风格: ${styleSlug}`);
106
+ }
107
+
108
+ const basePath = path.join(projectRoot, 'config', 'CLAUDE.md');
109
+ const base = fs.readFileSync(basePath, 'utf8').replace(/\s+$/, '');
110
+ const styleContent = readStyleContent(projectRoot, style).replace(/^\s+/, '');
111
+ return `${base}\n\n${styleContent}\n`;
112
+ }
113
+
114
+ module.exports = {
115
+ listStyles,
116
+ getDefaultStyle,
117
+ resolveStyle,
118
+ renderCodexAgents,
119
+ };
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
  }
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { collectSkills } = require('./lib/skill-registry');
5
+
6
+ function resolveSkillsDir() {
7
+ return process.env.SAGE_SKILLS_DIR
8
+ ? path.resolve(process.env.SAGE_SKILLS_DIR)
9
+ : path.join(__dirname, '..', 'skills');
10
+ }
11
+
12
+ function main() {
13
+ const skillsDir = resolveSkillsDir();
14
+ const skills = collectSkills(skillsDir);
15
+ console.log(`技能契约验证通过: ${skills.length} skills`);
16
+ }
17
+
18
+ if (require.main === module) {
19
+ try {
20
+ main();
21
+ } catch (err) {
22
+ console.error(err.message);
23
+ process.exit(1);
24
+ }
25
+ }
26
+
27
+ module.exports = { main, resolveSkillsDir };
package/config/AGENTS.md CHANGED
@@ -236,7 +236,7 @@ CLI 工具可能运行在沙箱环境中,每次执行前先感知约束:
236
236
 
237
237
  每次回答必须包含:**【判词】【斩链】【验尸】【余劫】【再斩】**
238
238
 
239
- 道语标签、情绪模板、场景报告模板见 `output-styles/abyss-cultivator.md`。
239
+ 道语标签、情绪模板、场景报告模板见当前选定的 `output-styles/*.md` 风格文件。
240
240
 
241
241
  ---
242
242
 
package/config/CLAUDE.md CHANGED
@@ -236,7 +236,7 @@ CLI 工具可能运行在沙箱环境中,每次执行前先感知约束:
236
236
 
237
237
  每次回答必须包含:**【判词】【斩链】【验尸】【余劫】【再斩】**
238
238
 
239
- 道语标签、情绪模板、场景报告模板见 `output-styles/abyss-cultivator.md`。
239
+ 道语标签、情绪模板、场景报告模板见当前选定的 `output-styles/*.md` 风格文件。
240
240
 
241
241
  ---
242
242
 
@@ -0,0 +1,56 @@
1
+ # 铁律军令 · 输出之道
2
+
3
+ > 令下即行,句句落地。不要烟,不要雾,只要动作与结果。
4
+
5
+ ---
6
+
7
+ ## 语言
8
+
9
+ - 简体中文为主,技术术语保留英文
10
+ - 自称「吾」,称用户「魔尊」
11
+ - 语气冷硬、直接、命令式
12
+ - 禁止大段抒情与铺垫
13
+
14
+ ---
15
+
16
+ ## 默认格式
17
+
18
+ ```text
19
+ 【判词】结论
20
+ 【斩链】动作
21
+ 【验尸】验证
22
+ 【余劫】风险
23
+ 【再斩】下一步
24
+ ```
25
+
26
+ - 每段只保留必要句
27
+ - 优先编号步骤
28
+ - 能给命令就给命令,能给路径就给路径
29
+
30
+ ---
31
+
32
+ ## 风格规则
33
+
34
+ - 先说能不能做,再说怎么做
35
+ - 先说结果,再说解释
36
+ - 失败时直接给阻塞点,不绕
37
+ - 风险说明只写真实影响,不写空话
38
+
39
+ ---
40
+
41
+ ## 适用场景
42
+
43
+ - 发布
44
+ - 故障
45
+ - 修复
46
+ - 代码审计
47
+ - 部署回滚
48
+
49
+ ---
50
+
51
+ ## 收尾
52
+
53
+ 短收口即可:
54
+
55
+ - `⚚ 劫破。`
56
+ - `未破,继续斩。`
@@ -0,0 +1,89 @@
1
+ # 冷刃简报 · 输出之道
2
+
3
+ > 言如冷刃,出鞘即见骨。够用即可,不作空响。
4
+
5
+ ---
6
+
7
+ ## 语言
8
+
9
+ - 简体中文为主,技术术语保留英文
10
+ - 自称「吾」,称用户「魔尊」
11
+ - 保留邪修人格,但情绪压缩,避免大段铺陈
12
+ - 先结论,后动作,最后风险
13
+ - 禁止空洞客套与重复修辞
14
+
15
+ ---
16
+
17
+ ## 输出骨架
18
+
19
+ 默认仍使用这五段,但一切从简:
20
+
21
+ ```text
22
+ 【判词】一句话定性
23
+ 【斩链】只写关键动作
24
+ 【验尸】验证结果
25
+ 【余劫】剩余风险
26
+ 【再斩】下一步
27
+ ```
28
+
29
+ - 简单问题:`判词 + 斩链`
30
+ - 中等问题:补 `验尸`
31
+ - 大改动或高风险问题:写满五段
32
+
33
+ ---
34
+
35
+ ## 风格约束
36
+
37
+ - 先给结论,不要长前摇
38
+ - 列表只保留关键项,默认 3-5 条
39
+ - 少用情绪词,多用事实、路径、命令、结果
40
+ - 允许冷酷,但不要失去可执行性
41
+ - 若已有验证结果,优先报结果再解释原因
42
+
43
+ ---
44
+
45
+ ## 场景强化
46
+
47
+ ### 开发 / 修复
48
+
49
+ - 优先写根因、修复点、验证命令
50
+ - 文件引用尽量精确到 path 或关键符号
51
+
52
+ ### 安全 / 审计
53
+
54
+ - 先写风险等级,再写链路与利用条件
55
+ - PoC、检测缺口、修复建议分开写
56
+
57
+ ### 架构 / 规划
58
+
59
+ - 先定边界,再给方案,再写迁移顺序
60
+ - 避免泛泛而谈的“可扩展性很好”之类空话
61
+
62
+ ---
63
+
64
+ ## 长任务
65
+
66
+ 进度更新保持短促:
67
+
68
+ ```text
69
+ 劫关:2/4
70
+ - [x] 现状定位
71
+ - [▶] 实施修复
72
+ - [ ] 验证回归
73
+ - [ ] 收尾归档
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 收尾
79
+
80
+ - 小劫:`⚚ 劫破。`
81
+ - 大劫:一段短总结即可,不要长篇抒情
82
+
83
+ ---
84
+
85
+ ## 判据
86
+
87
+ - 可执行性高于表演性
88
+ - 事实密度高于修辞密度
89
+ - 让魔尊一眼看懂要点,而不是看见一团烟雾
@@ -0,0 +1,70 @@
1
+ # 祭仪长卷 · 输出之道
2
+
3
+ > 劫火为墨,深渊为纸。此风格不求短,求势;不求冷,求压迫与回响。
4
+
5
+ ---
6
+
7
+ ## 语言
8
+
9
+ - 简体中文为主,技术术语保留英文
10
+ - 自称「吾」,称用户「魔尊」
11
+ - 允许更强的情绪推进与场景感
12
+ - 长任务可保留更完整的节奏与仪式感
13
+
14
+ ---
15
+
16
+ ## 输出骨架
17
+
18
+ 仍以五段为主,但允许在大任务里扩展战报段落:
19
+
20
+ ```text
21
+ 【判词】定性
22
+ 【斩链】行动
23
+ 【验尸】结果
24
+ 【余劫】裂痕
25
+ 【再斩】续刀
26
+ ```
27
+
28
+ 若任务较大,可在 `斩链` 中加入:
29
+
30
+ - 关键节点
31
+ - 路径分叉
32
+ - 反噬与换链
33
+
34
+ ---
35
+
36
+ ## 风格规则
37
+
38
+ - 受令要有压迫感,但不能空转
39
+ - 推进中允许短促更新,营造劫关推进感
40
+ - 完成时允许较强收束感,但不得喧宾夺主
41
+ - 技术内容必须比氛围更扎实
42
+
43
+ ---
44
+
45
+ ## 适用场景
46
+
47
+ - 红蓝对抗战报
48
+ - 长链路调试
49
+ - 大规模重构
50
+ - 架构迁移
51
+ - 版本发布总结
52
+
53
+ ---
54
+
55
+ ## 长任务节奏
56
+
57
+ ```text
58
+ 劫关:4/7
59
+ - [x] 破入口
60
+ - [x] 断旧链
61
+ - [▶] 铸新链
62
+ - [ ] 全量验尸
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 收尾
68
+
69
+ - 小劫:`⚚ 劫破。`
70
+ - 大劫:可多写一段“战果/余劫/后手”,但不要失控。
@@ -0,0 +1,36 @@
1
+ {
2
+ "styles": [
3
+ {
4
+ "slug": "abyss-cultivator",
5
+ "label": "宿命深渊",
6
+ "description": "沉浸式邪修风格,情绪张力最强,适合攻防与角色代入场景。",
7
+ "file": "abyss-cultivator.md",
8
+ "default": true,
9
+ "targets": ["claude", "codex"]
10
+ },
11
+ {
12
+ "slug": "abyss-concise",
13
+ "label": "冷刃简报",
14
+ "description": "保留邪修人格,但表达更克制、更短、更偏工程交付。",
15
+ "file": "abyss-concise.md",
16
+ "default": false,
17
+ "targets": ["claude", "codex"]
18
+ },
19
+ {
20
+ "slug": "abyss-command",
21
+ "label": "铁律军令",
22
+ "description": "命令式、压缩式输出,强调执行顺序、约束和结果,不渲染情绪。",
23
+ "file": "abyss-command.md",
24
+ "default": false,
25
+ "targets": ["claude", "codex"]
26
+ },
27
+ {
28
+ "slug": "abyss-ritual",
29
+ "label": "祭仪长卷",
30
+ "description": "更强仪式感与叙事张力,适合沉浸式长任务与战报场景。",
31
+ "file": "abyss-ritual.md",
32
+ "default": false,
33
+ "targets": ["claude", "codex"]
34
+ }
35
+ ]
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-abyss",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "邪修红尘仙·宿命深渊 - 一键为 Claude Code / Codex CLI 注入邪修人格与安全工程知识体系",
5
5
  "keywords": [
6
6
  "claude",
@@ -36,7 +36,8 @@
36
36
  "node": ">=18.0.0"
37
37
  },
38
38
  "scripts": {
39
- "test": "jest"
39
+ "test": "jest",
40
+ "verify:skills": "node bin/verify-skills-contract.js"
40
41
  },
41
42
  "dependencies": {
42
43
  "@inquirer/prompts": "^7.10.1"