@adversity/coding-tool-x 2.2.0 → 2.3.0

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,506 @@
1
+ /**
2
+ * 格式转换服务
3
+ *
4
+ * 支持 Claude Code ↔ Codex CLI 格式互转
5
+ * - Skills: SKILL.md 格式转换
6
+ * - Commands/Prompts: 命令/提示格式转换
7
+ */
8
+
9
+ // Codex CLI 限制
10
+ const CODEX_LIMITS = {
11
+ skillName: 100,
12
+ skillDescription: 500
13
+ };
14
+
15
+ /**
16
+ * 解析 YAML frontmatter
17
+ * 支持基本的 YAML 解析和嵌套 metadata 对象
18
+ */
19
+ function parseFrontmatter(content) {
20
+ const result = {
21
+ frontmatter: {},
22
+ body: content
23
+ };
24
+
25
+ // 移除 BOM
26
+ content = content.trim().replace(/^\uFEFF/, '');
27
+
28
+ // 解析 YAML frontmatter
29
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
30
+ if (!match) {
31
+ return result;
32
+ }
33
+
34
+ const frontmatterText = match[1];
35
+ result.body = match[2].trim();
36
+
37
+ // 解析 YAML(支持嵌套 metadata)
38
+ const lines = frontmatterText.split('\n');
39
+ let currentParent = null;
40
+
41
+ for (const line of lines) {
42
+ // 检测缩进(嵌套对象)
43
+ const indentMatch = line.match(/^(\s*)/);
44
+ const indent = indentMatch ? indentMatch[1].length : 0;
45
+
46
+ // 跳过空行
47
+ if (!line.trim()) continue;
48
+
49
+ const colonIndex = line.indexOf(':');
50
+ if (colonIndex === -1) continue;
51
+
52
+ const key = line.slice(0, colonIndex).trim();
53
+ let value = line.slice(colonIndex + 1).trim();
54
+
55
+ // 检测是否是嵌套对象开始(值为空)
56
+ if (!value && indent === 0) {
57
+ currentParent = key;
58
+ result.frontmatter[key] = {};
59
+ continue;
60
+ }
61
+
62
+ // 处理嵌套属性
63
+ if (indent > 0 && currentParent && typeof result.frontmatter[currentParent] === 'object') {
64
+ // 去除引号
65
+ if ((value.startsWith('"') && value.endsWith('"')) ||
66
+ (value.startsWith("'") && value.endsWith("'"))) {
67
+ value = value.slice(1, -1);
68
+ }
69
+ result.frontmatter[currentParent][key] = value;
70
+ continue;
71
+ }
72
+
73
+ // 重置当前父级
74
+ currentParent = null;
75
+
76
+ // 去除引号
77
+ if ((value.startsWith('"') && value.endsWith('"')) ||
78
+ (value.startsWith("'") && value.endsWith("'"))) {
79
+ value = value.slice(1, -1);
80
+ }
81
+
82
+ result.frontmatter[key] = value;
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * 生成 YAML frontmatter 字符串
90
+ */
91
+ function generateFrontmatter(data, format = 'claude') {
92
+ const lines = ['---'];
93
+
94
+ if (format === 'codex') {
95
+ // Codex 格式: name, description 必须,metadata 可选
96
+ if (data.name) {
97
+ lines.push(`name: "${escapeYamlString(data.name)}"`);
98
+ }
99
+ if (data.description) {
100
+ lines.push(`description: "${escapeYamlString(data.description)}"`);
101
+ }
102
+ if (data.metadata && Object.keys(data.metadata).length > 0) {
103
+ lines.push('metadata:');
104
+ for (const [key, value] of Object.entries(data.metadata)) {
105
+ lines.push(` ${key}: "${escapeYamlString(value)}"`);
106
+ }
107
+ }
108
+ // Codex commands/prompts 特有字段
109
+ if (data['argument-hint']) {
110
+ lines.push(`argument-hint: ${data['argument-hint']}`);
111
+ }
112
+ } else {
113
+ // Claude Code 格式
114
+ if (data.name) {
115
+ lines.push(`name: "${escapeYamlString(data.name)}"`);
116
+ }
117
+ if (data.description) {
118
+ lines.push(`description: "${escapeYamlString(data.description)}"`);
119
+ }
120
+ if (data.license) {
121
+ lines.push(`license: "${escapeYamlString(data.license)}"`);
122
+ }
123
+ // Claude Code commands 特有字段
124
+ if (data['allowed-tools']) {
125
+ lines.push(`allowed-tools: ${data['allowed-tools']}`);
126
+ }
127
+ if (data['argument-hint']) {
128
+ lines.push(`argument-hint: ${data['argument-hint']}`);
129
+ }
130
+ if (data.model) {
131
+ lines.push(`model: ${data.model}`);
132
+ }
133
+ if (data.context) {
134
+ lines.push(`context: ${data.context}`);
135
+ }
136
+ if (data.agent) {
137
+ lines.push(`agent: ${data.agent}`);
138
+ }
139
+ }
140
+
141
+ lines.push('---');
142
+ return lines.join('\n');
143
+ }
144
+
145
+ /**
146
+ * 转义 YAML 字符串中的特殊字符
147
+ */
148
+ function escapeYamlString(str) {
149
+ if (!str) return '';
150
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
151
+ }
152
+
153
+ /**
154
+ * 检测 Skill 格式
155
+ * @returns {'claude' | 'codex' | 'unknown'}
156
+ */
157
+ function detectSkillFormat(content) {
158
+ const { frontmatter } = parseFrontmatter(content);
159
+
160
+ // Codex 特征: 有 metadata 对象
161
+ if (frontmatter.metadata && typeof frontmatter.metadata === 'object') {
162
+ return 'codex';
163
+ }
164
+
165
+ // Claude Code 特征: 有 allowed-tools 或 license
166
+ if (frontmatter['allowed-tools'] || frontmatter.license) {
167
+ return 'claude';
168
+ }
169
+
170
+ // 两者都有 name 和 description,无法区分时默认 claude
171
+ if (frontmatter.name && frontmatter.description) {
172
+ return 'claude';
173
+ }
174
+
175
+ return 'unknown';
176
+ }
177
+
178
+ /**
179
+ * 检测 Command/Prompt 格式
180
+ * @returns {'claude' | 'codex' | 'unknown'}
181
+ */
182
+ function detectCommandFormat(content) {
183
+ const { frontmatter } = parseFrontmatter(content);
184
+
185
+ // Claude Code 特征: allowed-tools, model, context, agent, hooks
186
+ if (frontmatter['allowed-tools'] || frontmatter.model ||
187
+ frontmatter.context || frontmatter.agent || frontmatter.hooks) {
188
+ return 'claude';
189
+ }
190
+
191
+ // Codex 特征: 只有 description 和 argument-hint
192
+ if (frontmatter.description && !frontmatter['allowed-tools']) {
193
+ return 'codex';
194
+ }
195
+
196
+ return 'unknown';
197
+ }
198
+
199
+ /**
200
+ * 转换 Skill: Claude Code → Codex CLI
201
+ */
202
+ function convertSkillToCodex(claudeContent, options = {}) {
203
+ const { frontmatter, body } = parseFrontmatter(claudeContent);
204
+ const warnings = [];
205
+
206
+ // 处理字段
207
+ const codexData = {
208
+ name: frontmatter.name || '',
209
+ description: frontmatter.description || '',
210
+ metadata: {}
211
+ };
212
+
213
+ // 检查并截断 name
214
+ if (codexData.name.length > CODEX_LIMITS.skillName) {
215
+ warnings.push(`name 超过 ${CODEX_LIMITS.skillName} 字符,已截断`);
216
+ codexData.name = codexData.name.slice(0, CODEX_LIMITS.skillName);
217
+ }
218
+
219
+ // 检查并截断 description
220
+ if (codexData.description.length > CODEX_LIMITS.skillDescription) {
221
+ warnings.push(`description 超过 ${CODEX_LIMITS.skillDescription} 字符,已截断`);
222
+ codexData.description = codexData.description.slice(0, CODEX_LIMITS.skillDescription);
223
+ }
224
+
225
+ // 不支持的字段警告
226
+ if (frontmatter['allowed-tools']) {
227
+ warnings.push('allowed-tools 字段在 Codex 中不支持,已忽略');
228
+ }
229
+
230
+ // 生成 Codex 格式内容
231
+ const codexFrontmatter = generateFrontmatter(codexData, 'codex');
232
+ const codexContent = codexFrontmatter + '\n\n' + body;
233
+
234
+ return {
235
+ content: codexContent,
236
+ warnings,
237
+ format: 'codex'
238
+ };
239
+ }
240
+
241
+ /**
242
+ * 转换 Skill: Codex CLI → Claude Code
243
+ */
244
+ function convertSkillToClaude(codexContent, options = {}) {
245
+ const { frontmatter, body } = parseFrontmatter(codexContent);
246
+ const warnings = [];
247
+
248
+ // 处理字段
249
+ const claudeData = {
250
+ name: frontmatter.name || '',
251
+ description: frontmatter.description || ''
252
+ };
253
+
254
+ // 保留 metadata.short-description(如果有)作为注释
255
+ if (frontmatter.metadata && frontmatter.metadata['short-description']) {
256
+ // 可以选择将其添加到 description 或作为单独字段
257
+ warnings.push(`metadata.short-description 已保留: "${frontmatter.metadata['short-description']}"`);
258
+ }
259
+
260
+ // 生成 Claude Code 格式内容
261
+ const claudeFrontmatter = generateFrontmatter(claudeData, 'claude');
262
+ const claudeContent = claudeFrontmatter + '\n\n' + body;
263
+
264
+ return {
265
+ content: claudeContent,
266
+ warnings,
267
+ format: 'claude'
268
+ };
269
+ }
270
+
271
+ /**
272
+ * 转换 Command: Claude Code → Codex CLI (Custom Prompt)
273
+ */
274
+ function convertCommandToCodex(claudeContent, options = {}) {
275
+ const { frontmatter, body } = parseFrontmatter(claudeContent);
276
+ const warnings = [];
277
+
278
+ // 处理字段
279
+ const codexData = {
280
+ description: frontmatter.description || ''
281
+ };
282
+
283
+ // argument-hint 两者都支持
284
+ if (frontmatter['argument-hint']) {
285
+ codexData['argument-hint'] = frontmatter['argument-hint'];
286
+ }
287
+
288
+ // 不支持的字段警告
289
+ const unsupportedFields = ['allowed-tools', 'model', 'context', 'agent', 'hooks'];
290
+ for (const field of unsupportedFields) {
291
+ if (frontmatter[field]) {
292
+ warnings.push(`${field} 字段在 Codex 中不支持,已忽略`);
293
+ }
294
+ }
295
+
296
+ // 检查 body 中的 Claude Code 特有语法
297
+ let processedBody = body;
298
+
299
+ // 检测 Bash 执行语法 !`command`
300
+ if (/!\`[^`]+\`/.test(body)) {
301
+ warnings.push('Bash 执行语法 !`command` 在 Codex 中不支持,请手动替换');
302
+ }
303
+
304
+ // 检测文件引用语法 @filepath
305
+ if (/@[^\s]+/.test(body)) {
306
+ warnings.push('文件引用语法 @filepath 在 Codex 中不支持,请手动替换');
307
+ }
308
+
309
+ // 生成 Codex 格式内容
310
+ let codexContent = '';
311
+ if (codexData.description || codexData['argument-hint']) {
312
+ codexContent = generateFrontmatter(codexData, 'codex') + '\n\n';
313
+ }
314
+ codexContent += processedBody;
315
+
316
+ return {
317
+ content: codexContent,
318
+ warnings,
319
+ format: 'codex'
320
+ };
321
+ }
322
+
323
+ /**
324
+ * 转换 Command: Codex CLI (Custom Prompt) → Claude Code
325
+ */
326
+ function convertCommandToClaude(codexContent, options = {}) {
327
+ const { frontmatter, body } = parseFrontmatter(codexContent);
328
+ const warnings = [];
329
+
330
+ // 处理字段
331
+ const claudeData = {
332
+ description: frontmatter.description || ''
333
+ };
334
+
335
+ // argument-hint 两者都支持
336
+ if (frontmatter['argument-hint']) {
337
+ claudeData['argument-hint'] = frontmatter['argument-hint'];
338
+ }
339
+
340
+ // Codex 命名参数 $KEY 在 Claude Code 中也支持,无需转换
341
+
342
+ // 生成 Claude Code 格式内容
343
+ let claudeContent = '';
344
+ if (claudeData.description || claudeData['argument-hint']) {
345
+ claudeContent = generateFrontmatter(claudeData, 'claude') + '\n\n';
346
+ }
347
+ claudeContent += body;
348
+
349
+ return {
350
+ content: claudeContent,
351
+ warnings,
352
+ format: 'claude'
353
+ };
354
+ }
355
+
356
+ /**
357
+ * 批量转换 Skills
358
+ */
359
+ function convertSkillsBatch(skills, targetFormat) {
360
+ const results = [];
361
+
362
+ for (const skill of skills) {
363
+ try {
364
+ const converted = targetFormat === 'codex'
365
+ ? convertSkillToCodex(skill.content)
366
+ : convertSkillToClaude(skill.content);
367
+
368
+ results.push({
369
+ name: skill.name,
370
+ success: true,
371
+ ...converted
372
+ });
373
+ } catch (err) {
374
+ results.push({
375
+ name: skill.name,
376
+ success: false,
377
+ error: err.message
378
+ });
379
+ }
380
+ }
381
+
382
+ return results;
383
+ }
384
+
385
+ /**
386
+ * 批量转换 Commands
387
+ */
388
+ function convertCommandsBatch(commands, targetFormat) {
389
+ const results = [];
390
+
391
+ for (const command of commands) {
392
+ try {
393
+ const converted = targetFormat === 'codex'
394
+ ? convertCommandToCodex(command.content)
395
+ : convertCommandToClaude(command.content);
396
+
397
+ results.push({
398
+ name: command.name,
399
+ success: true,
400
+ ...converted
401
+ });
402
+ } catch (err) {
403
+ results.push({
404
+ name: command.name,
405
+ success: false,
406
+ error: err.message
407
+ });
408
+ }
409
+ }
410
+
411
+ return results;
412
+ }
413
+
414
+ /**
415
+ * 解析 Skill 内容(支持双格式)
416
+ */
417
+ function parseSkillContent(content) {
418
+ const { frontmatter, body } = parseFrontmatter(content);
419
+ const format = detectSkillFormat(content);
420
+
421
+ const result = {
422
+ name: frontmatter.name || '',
423
+ description: frontmatter.description || '',
424
+ body,
425
+ fullContent: content,
426
+ format
427
+ };
428
+
429
+ // 处理 Codex 特有字段
430
+ if (frontmatter.metadata) {
431
+ result.metadata = frontmatter.metadata;
432
+ if (frontmatter.metadata['short-description']) {
433
+ result.shortDescription = frontmatter.metadata['short-description'];
434
+ }
435
+ }
436
+
437
+ // 处理 Claude Code 特有字段
438
+ if (frontmatter['allowed-tools']) {
439
+ result.allowedTools = frontmatter['allowed-tools'];
440
+ }
441
+ if (frontmatter.license) {
442
+ result.license = frontmatter.license;
443
+ }
444
+
445
+ return result;
446
+ }
447
+
448
+ /**
449
+ * 解析 Command 内容(支持双格式)
450
+ */
451
+ function parseCommandContent(content) {
452
+ const { frontmatter, body } = parseFrontmatter(content);
453
+ const format = detectCommandFormat(content);
454
+
455
+ const result = {
456
+ description: frontmatter.description || '',
457
+ argumentHint: frontmatter['argument-hint'] || '',
458
+ body,
459
+ fullContent: content,
460
+ format
461
+ };
462
+
463
+ // 处理 Claude Code 特有字段
464
+ if (frontmatter['allowed-tools']) {
465
+ result.allowedTools = frontmatter['allowed-tools'];
466
+ }
467
+ if (frontmatter.model) {
468
+ result.model = frontmatter.model;
469
+ }
470
+ if (frontmatter.context) {
471
+ result.context = frontmatter.context;
472
+ }
473
+ if (frontmatter.agent) {
474
+ result.agent = frontmatter.agent;
475
+ }
476
+ if (frontmatter.hooks) {
477
+ result.hooks = frontmatter.hooks;
478
+ }
479
+
480
+ return result;
481
+ }
482
+
483
+ module.exports = {
484
+ // 格式检测
485
+ detectSkillFormat,
486
+ detectCommandFormat,
487
+
488
+ // Skills 转换
489
+ convertSkillToCodex,
490
+ convertSkillToClaude,
491
+ convertSkillsBatch,
492
+
493
+ // Commands 转换
494
+ convertCommandToCodex,
495
+ convertCommandToClaude,
496
+ convertCommandsBatch,
497
+
498
+ // 通用解析(支持双格式)
499
+ parseSkillContent,
500
+ parseCommandContent,
501
+ parseFrontmatter,
502
+ generateFrontmatter,
503
+
504
+ // 常量
505
+ CODEX_LIMITS
506
+ };