@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 +1 -1
- package/scripts/install.js +126 -36
- package/scripts/test-auto-detect.js +125 -0
package/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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');
|