@sokeai/cli 1.0.19 → 1.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sokeai/cli",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "授客AI官方CLI工具 - 支持AI Agent Skills",
5
5
  "bin": {
6
6
  "soke-cli": "scripts/run.js"
@@ -89,13 +89,31 @@ function detectSokeclawWorkspaceSkillsDirs() {
89
89
  return dirs;
90
90
  }
91
91
 
92
+ /**
93
+ * 自动检测 skills 目录中的所有 skill
94
+ * 只包含以 'soke-' 开头的目录
95
+ */
96
+ function detectSkillNames(packagedSkillsDir) {
97
+ if (!fs.existsSync(packagedSkillsDir)) return [];
98
+
99
+ try {
100
+ const entries = fs.readdirSync(packagedSkillsDir, { withFileTypes: true });
101
+ return entries
102
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('soke-'))
103
+ .map(entry => entry.name)
104
+ .sort(); // 排序确保一致性
105
+ } catch (_) {
106
+ return [];
107
+ }
108
+ }
109
+
92
110
  function syncSkillsToSokeclawWorkspace() {
93
111
  const packageRoot = path.join(__dirname, '..');
94
112
  const packagedSkillsDir = path.join(packageRoot, 'skills');
95
113
  if (!fs.existsSync(packagedSkillsDir)) return;
96
114
 
97
115
  const targetDirs = detectSokeclawWorkspaceSkillsDirs();
98
- const skillNames = ['soke-shared', 'soke-exam'];
116
+ const skillNames = detectSkillNames(packagedSkillsDir);
99
117
 
100
118
  for (const targetDir of targetDirs) {
101
119
  try {
@@ -126,6 +144,76 @@ function upsertSkillRegistryEntry(registry, entry) {
126
144
  registry.skills.push(entry);
127
145
  }
128
146
 
147
+ /**
148
+ * 从 SKILL.md 文件中解析元数据
149
+ */
150
+ function parseSkillMetadata(skillDir) {
151
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
152
+ if (!fs.existsSync(skillMdPath)) {
153
+ return null;
154
+ }
155
+
156
+ try {
157
+ const content = fs.readFileSync(skillMdPath, 'utf8');
158
+
159
+ // 解析 frontmatter (YAML)
160
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
161
+ if (!frontmatterMatch) return null;
162
+
163
+ const frontmatter = frontmatterMatch[1];
164
+ const metadata = {};
165
+
166
+ // 解析 name
167
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
168
+ if (nameMatch) metadata.name = nameMatch[1].trim();
169
+
170
+ // 解析 summary (用作 displayName)
171
+ const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
172
+ if (summaryMatch) metadata.summary = summaryMatch[1].trim();
173
+
174
+ // 解析 description
175
+ const descMatch = frontmatter.match(/^description:\s*["'](.+)["']$/m);
176
+ if (descMatch) {
177
+ metadata.description = descMatch[1].trim();
178
+ } else {
179
+ const descMatch2 = frontmatter.match(/^description:\s*(.+)$/m);
180
+ if (descMatch2) metadata.description = descMatch2[1].trim();
181
+ }
182
+
183
+ // 解析 version
184
+ const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
185
+ if (versionMatch) metadata.version = versionMatch[1].trim();
186
+
187
+ // 解析 metadata.requires.bins
188
+ const binsMatch = frontmatter.match(/bins:\s*\[(.+?)\]/);
189
+ if (binsMatch) {
190
+ metadata.bins = binsMatch[1].split(',').map(b => b.trim().replace(/['"]/g, ''));
191
+ }
192
+
193
+ return metadata;
194
+ } catch (_) {
195
+ return null;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * 根据 skill 名称推断 emoji
201
+ */
202
+ function inferSkillEmoji(skillName) {
203
+ const emojiMap = {
204
+ 'soke-exam': '📝',
205
+ 'soke-course': '📚',
206
+ 'soke-shared': '🔧',
207
+ 'soke-user': '👤',
208
+ 'soke-contact': '📇',
209
+ 'soke-department': '🏢',
210
+ 'soke-approval': '✅',
211
+ 'soke-attendance': '📅',
212
+ 'soke-report': '📊'
213
+ };
214
+ return emojiMap[skillName] || '📦';
215
+ }
216
+
129
217
  function syncSkillsToWorkclawRegistry() {
130
218
  const homeDir = os.homedir();
131
219
  const workclawRootDir = path.join(homeDir, '.workclaw');
@@ -165,48 +253,50 @@ function syncSkillsToWorkclawRegistry() {
165
253
  ? existingSkills[0].source.type
166
254
  : 'local';
167
255
 
168
- const skillNames = ['soke-shared', 'soke-exam'];
256
+ // 自动检测所有 skills
257
+ const skillNames = detectSkillNames(packagedSkillsDir);
258
+
259
+ // 复制所有 skills
169
260
  for (const skillName of skillNames) {
170
261
  const src = path.join(packagedSkillsDir, skillName);
171
262
  const dest = path.join(workclawSkillInstallDir, skillName);
172
263
  if (fs.existsSync(src)) copyDirRecursive(src, dest);
173
264
  }
174
265
 
175
- upsertSkillRegistryEntry(registry, {
176
- id: 'skill:soke-exam',
177
- name: 'soke-exam',
178
- displayName: '授客考试管理',
179
- description: '授客考试管理:查询考试、考试分类、考试用户成绩、考试详情。',
180
- source: { type: defaultSourceType, slug: '', url: '' },
181
- install: {
182
- path: path.join(workclawSkillInstallDir, 'soke-exam'),
183
- installedAt: '',
184
- updatedAt: '',
185
- version: '1.0.0'
186
- },
187
- state: { enabled: true, health: 'ok', lastError: '' },
188
- runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
189
- security: { riskLevel: 'normal', requiresApproval: false },
190
- metadata: { emoji: '📝', homepage: '', requires: { bins: ['soke-cli'] } }
191
- });
266
+ // 自动注册所有 skills
267
+ for (const skillName of skillNames) {
268
+ const skillDir = path.join(packagedSkillsDir, skillName);
269
+ const metadata = parseSkillMetadata(skillDir);
192
270
 
193
- upsertSkillRegistryEntry(registry, {
194
- id: 'skill:soke-shared',
195
- name: 'soke-shared',
196
- displayName: 'soke-shared 共享规则',
197
- description: '授客CLI共享基础:配置、登录、权限管理、错误处理、安全规则。',
198
- source: { type: defaultSourceType, slug: '', url: '' },
199
- install: {
200
- path: path.join(workclawSkillInstallDir, 'soke-shared'),
201
- installedAt: '',
202
- updatedAt: '',
203
- version: '1.0.0'
204
- },
205
- state: { enabled: true, health: 'ok', lastError: '' },
206
- runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
207
- security: { riskLevel: 'normal', requiresApproval: false },
208
- metadata: { emoji: '🔧', homepage: '', requires: {} }
209
- });
271
+ if (!metadata || !metadata.name) {
272
+ console.warn(`警告: 无法解析 ${skillName} 的元数据,跳过注册`);
273
+ continue;
274
+ }
275
+
276
+ const displayName = metadata.summary || metadata.name;
277
+ const description = metadata.description || `${displayName} - 授客AI CLI工具`;
278
+ const version = metadata.version || '1.0.0';
279
+ const emoji = inferSkillEmoji(skillName);
280
+ const requires = metadata.bins ? { bins: metadata.bins } : {};
281
+
282
+ upsertSkillRegistryEntry(registry, {
283
+ id: `skill:${skillName}`,
284
+ name: skillName,
285
+ displayName: displayName,
286
+ description: description,
287
+ source: { type: defaultSourceType, slug: '', url: '' },
288
+ install: {
289
+ path: path.join(workclawSkillInstallDir, skillName),
290
+ installedAt: '',
291
+ updatedAt: '',
292
+ version: version
293
+ },
294
+ state: { enabled: true, health: 'ok', lastError: '' },
295
+ runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
296
+ security: { riskLevel: 'normal', requiresApproval: false },
297
+ metadata: { emoji: emoji, homepage: '', requires: requires }
298
+ });
299
+ }
210
300
 
211
301
  try {
212
302
  fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 测试 install.js 的自动检测功能
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ // 复制 detectSkillNames 函数
11
+ function detectSkillNames(packagedSkillsDir) {
12
+ if (!fs.existsSync(packagedSkillsDir)) return [];
13
+
14
+ try {
15
+ const entries = fs.readdirSync(packagedSkillsDir, { withFileTypes: true });
16
+ return entries
17
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('soke-'))
18
+ .map(entry => entry.name)
19
+ .sort();
20
+ } catch (_) {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ // 复制 parseSkillMetadata 函数
26
+ function parseSkillMetadata(skillDir) {
27
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
28
+ if (!fs.existsSync(skillMdPath)) {
29
+ return null;
30
+ }
31
+
32
+ try {
33
+ const content = fs.readFileSync(skillMdPath, 'utf8');
34
+
35
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
36
+ if (!frontmatterMatch) return null;
37
+
38
+ const frontmatter = frontmatterMatch[1];
39
+ const metadata = {};
40
+
41
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
42
+ if (nameMatch) metadata.name = nameMatch[1].trim();
43
+
44
+ const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
45
+ if (summaryMatch) metadata.summary = summaryMatch[1].trim();
46
+
47
+ const descMatch = frontmatter.match(/^description:\s*["'](.+)["']$/m);
48
+ if (descMatch) {
49
+ metadata.description = descMatch[1].trim();
50
+ } else {
51
+ const descMatch2 = frontmatter.match(/^description:\s*(.+)$/m);
52
+ if (descMatch2) metadata.description = descMatch2[1].trim();
53
+ }
54
+
55
+ const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
56
+ if (versionMatch) metadata.version = versionMatch[1].trim();
57
+
58
+ const binsMatch = frontmatter.match(/bins:\s*\[(.+?)\]/);
59
+ if (binsMatch) {
60
+ metadata.bins = binsMatch[1].split(',').map(b => b.trim().replace(/['"]/g, ''));
61
+ }
62
+
63
+ return metadata;
64
+ } catch (_) {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ // 测试
70
+ const packageRoot = path.join(__dirname, '..');
71
+ const skillsDir = path.join(packageRoot, 'skills');
72
+
73
+ console.log('🔍 测试自动检测功能\n');
74
+ console.log('Skills 目录:', skillsDir);
75
+ console.log('');
76
+
77
+ // 检测所有 skills
78
+ const skillNames = detectSkillNames(skillsDir);
79
+
80
+ console.log(`✅ 检测到 ${skillNames.length} 个 skills:\n`);
81
+
82
+ // 显示每个 skill 的详细信息
83
+ for (const skillName of skillNames) {
84
+ const skillDir = path.join(skillsDir, skillName);
85
+ const metadata = parseSkillMetadata(skillDir);
86
+
87
+ console.log(`📦 ${skillName}`);
88
+
89
+ if (metadata) {
90
+ console.log(` 名称: ${metadata.name || '未知'}`);
91
+ console.log(` 摘要: ${metadata.summary || '未知'}`);
92
+ console.log(` 版本: ${metadata.version || '未知'}`);
93
+ console.log(` 依赖: ${metadata.bins ? metadata.bins.join(', ') : '无'}`);
94
+ console.log(` 描述: ${metadata.description ? metadata.description.substring(0, 60) + '...' : '未知'}`);
95
+ } else {
96
+ console.log(' ⚠️ 无法解析 SKILL.md');
97
+ }
98
+
99
+ console.log('');
100
+ }
101
+
102
+ // 验证结果
103
+ console.log('📊 验证结果:\n');
104
+
105
+ const expectedSkills = ['soke-course', 'soke-exam', 'soke-shared'];
106
+ const missingSkills = expectedSkills.filter(s => !skillNames.includes(s));
107
+ const extraSkills = skillNames.filter(s => !expectedSkills.includes(s));
108
+
109
+ if (missingSkills.length > 0) {
110
+ console.log(`❌ 缺少的 skills: ${missingSkills.join(', ')}`);
111
+ } else {
112
+ console.log('✅ 所有预期的 skills 都已检测到');
113
+ }
114
+
115
+ if (extraSkills.length > 0) {
116
+ console.log(`ℹ️ 额外的 skills: ${extraSkills.join(', ')}`);
117
+ }
118
+
119
+ console.log('');
120
+ console.log('🎉 测试完成!');
121
+ console.log('');
122
+ console.log('💡 提示:');
123
+ console.log(' - 新增 skill 时,只需在 skills/ 目录下创建 soke-* 目录');
124
+ console.log(' - 确保每个 skill 都有 SKILL.md 文件,包含完整的 frontmatter');
125
+ console.log(' - install.js 会自动检测并注册所有 skills');