@sokeai/cli 1.0.37 → 1.0.41
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 +24 -1
- package/package.json +1 -1
- package/scripts/build-binaries.sh +30 -1
- package/scripts/ci/check-legacy-constants.sh +27 -0
- package/scripts/ci/check-yaml-env-whitelist.sh +33 -0
- package/scripts/install.js +110 -52
- package/scripts/local-test.js +85 -10
- package/scripts/local-test.sh +7 -52
- package/scripts/mcp-stdout-scan.sh +46 -0
- package/scripts/push.sh +1 -1
- package/scripts/regress-auth.sh +56 -0
- package/scripts/regress-baseline.md +84 -0
- package/scripts/release.sh +35 -8
- package/scripts/test-auto-detect.js +48 -71
- package/scripts/test-local-reconcile.js +55 -0
- package/skills/soke-business-training-report/SKILL.md +58 -0
- package/skills/soke-course/SKILL.md +64 -0
- package/skills/soke-course/templates/batch-create-from-excel.md +75 -0
- package/skills/soke-learning-profile/SKILL.md +1 -0
- package/skills/soke-exam/SKILL.md +0 -318
- package/skills/soke-exam/references/exam-get-exam-user.md +0 -212
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
3. **业务模块快捷命令**
|
|
28
28
|
- 针对授客AI核心业务场景提供了丰富的快捷命令集:
|
|
29
29
|
- **通讯录 (contact)**: 部门与用户查询、管理、搜索等。
|
|
30
|
-
- **课程 (course)**:
|
|
30
|
+
- **课程 (course)**: 课程列表、分类、学习记录、人脸识别记录等;含 `+batch-create-from-excel` 从 Excel 批量创建课程并上传课件(ffmpeg 可选,系统已装则启用视频抽帧封面,未装则降级到默认封面)。
|
|
31
31
|
- **考试 (exam)**: 考试列表、分类、考试成绩与记录查询。
|
|
32
32
|
- **学习档案 (learning-profile)**: 学员学习档案查询、学习情况统计。
|
|
33
33
|
- **学习地图 (learning-map)**: 学习地图、阶段、任务查询与分配。
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
4. **AI Agent Skills**
|
|
40
40
|
- 提供 AI Agent 技能(Skills),使 AI 助手能够自动发现和调用 CLI 功能。
|
|
41
41
|
- 支持自然语言交互,无需记忆复杂的命令参数。
|
|
42
|
+
- 支持维护者删除受管 Skill,并通过本地同步与升级安装自动清理废弃 Skill。
|
|
42
43
|
|
|
43
44
|
## 安装方法
|
|
44
45
|
|
|
@@ -164,6 +165,28 @@ soke-cli api GET /users/me
|
|
|
164
165
|
soke-cli api POST /some/endpoint --data '{"key": "value"}'
|
|
165
166
|
```
|
|
166
167
|
|
|
168
|
+
### 删除受管 Skill(维护者)
|
|
169
|
+
|
|
170
|
+
如果你需要从当前仓库中下线一个受管 Skill,可以执行:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
soke-cli delete-skill soke-business-training-report
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
删除后再执行:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
bash ./scripts/local-test.sh
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
本地 Agent 中由 `soke-cli` 管理的过期 `soke-*` Skills 会被自动清理。客户后续执行:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm install -g @sokeai/cli@latest
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
也会按当前发布包中的 Skill 集合自动清理废弃的受管 Skill。
|
|
189
|
+
|
|
167
190
|
## 开发指南
|
|
168
191
|
|
|
169
192
|
### 从接口封装到发布的完整流程
|
package/package.json
CHANGED
|
@@ -15,8 +15,37 @@ NC='\033[0m' # No Color
|
|
|
15
15
|
VERSION=$(node -p "require('./package.json').version")
|
|
16
16
|
echo -e "${GREEN}开始编译 soke-cli v${VERSION}${NC}"
|
|
17
17
|
|
|
18
|
+
# task 11.2 — OAuth 凭证通过 env 注入(替代原 sed 改 Go 源的方式)。
|
|
19
|
+
#
|
|
20
|
+
# 重要:vendor.yaml 的 ${SOKE_CLIENT_ID} / ${SOKE_CLIENT_SECRET} 由
|
|
21
|
+
# sokeai/provider.go 在 init() 阶段 Interpolate,**取的是产物运行时的 env**,
|
|
22
|
+
# 而不是 build 期。因此:
|
|
23
|
+
#
|
|
24
|
+
# * 本地开发:用户在 shell 里 export SOKE_CLIENT_ID / SOKE_CLIENT_SECRET,
|
|
25
|
+
# 然后 go run / 已 build 的 ./bin/soke-cli 启动时 init() 取到。
|
|
26
|
+
# * Release 二进制:client_id / client_secret 不在 build 阶段嵌入产物。
|
|
27
|
+
# 发版 CI 应当在制作 NPM 包时附带一个 wrapper(scripts/run.js)在启动
|
|
28
|
+
# soke-cli 前 setenv 注入(具体方式由 release-lead 在发版 CI 里定),
|
|
29
|
+
# 或要求最终用户自己 export(仅适用于内部分发场景)。
|
|
30
|
+
#
|
|
31
|
+
# 本脚本只做 sanity check + 把当前 shell env 透传到 go build(用于自动化
|
|
32
|
+
# 测试场景中,build 后立即跑 ./bin/soke-cli auth login 走流程)。
|
|
33
|
+
if [ -z "${SOKE_CLIENT_ID:-}" ] || [ -z "${SOKE_CLIENT_SECRET:-}" ]; then
|
|
34
|
+
if [ "${SOKE_CLI_RELEASE:-}" = "1" ]; then
|
|
35
|
+
echo -e "${RED}[FATAL] release 模式必须设置 SOKE_CLIENT_ID / SOKE_CLIENT_SECRET${NC}"
|
|
36
|
+
echo -e "${RED} 二进制本身不嵌入凭证,run.js wrapper 也需要相应的注入逻辑${NC}"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
echo -e "${YELLOW}[警告] SOKE_CLIENT_ID / SOKE_CLIENT_SECRET 未设置;${NC}"
|
|
40
|
+
echo -e "${YELLOW} 本地 build 后直接 ./bin/soke-cli auth login 会 fail-fast。${NC}"
|
|
41
|
+
fi
|
|
42
|
+
export SOKE_CLIENT_ID SOKE_CLIENT_SECRET
|
|
43
|
+
|
|
18
44
|
# 编译参数
|
|
19
|
-
|
|
45
|
+
# -X 注入路径必须匹配 internal/version.Version 所在包,
|
|
46
|
+
# 此前误写为 main.Version,导致 --version 始终落回 checker.go 里
|
|
47
|
+
# 的 hardcoded 默认值。
|
|
48
|
+
LDFLAGS="-s -w -X codeup.aliyun.com/5edbc121d1d1abe63b55f1c7/soke/soke-cli/internal/version.Version=${VERSION}"
|
|
20
49
|
|
|
21
50
|
# 创建输出目录
|
|
22
51
|
BIN_DIR="bin"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# task 9.1 — 拦截误引入的 dws-cli upstream 旧常量。
|
|
3
|
+
#
|
|
4
|
+
# 故意提一个引入旧常量的 bad PR 时,CI 该步骤应当失败。
|
|
5
|
+
# 允许出现在 openspec/ 历史 doc 与 docs/dead-code-audit-*.md。
|
|
6
|
+
#
|
|
7
|
+
# 用法:bash scripts/ci/check-legacy-constants.sh
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
PATTERN='OpenDevURL\|AuthorizeURL\|DefaultMCPBaseURL\|DefaultClientID\|DefaultClientSecret\|<YOUR_CLIENT_'
|
|
12
|
+
|
|
13
|
+
# 只扫源码目录;openspec/docs/scripts 历史档案允许出现。
|
|
14
|
+
hits=$(grep -rn -E "$PATTERN" cmd/ internal/ extension/ 2>/dev/null \
|
|
15
|
+
| grep -v vendorplugin/ \
|
|
16
|
+
| grep -v "_test.go" \
|
|
17
|
+
|| true)
|
|
18
|
+
|
|
19
|
+
if [ -n "$hits" ]; then
|
|
20
|
+
echo "[FAIL] 检测到 dws-cli 旧常量残留:"
|
|
21
|
+
echo "$hits"
|
|
22
|
+
echo
|
|
23
|
+
echo "请改用 vendor.Active().Endpoints() / OAuth() 读取(见 cmd/user_auth/vendor.go)。"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
echo "[OK] 无 dws-cli 旧常量残留。"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# task 9.2 — 校验 extension/vendorplugin/builtin/**/vendor.yaml 内 ${VAR}
|
|
3
|
+
# 占位符全部符合白名单(^SOKE_[A-Z0-9_]+$)。
|
|
4
|
+
#
|
|
5
|
+
# 故意写一个 ${HOME} 进 vendor.yaml 时,本步骤应当 FAIL。
|
|
6
|
+
#
|
|
7
|
+
# 用法:bash scripts/ci/check-yaml-env-whitelist.sh
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
WHITELIST_RE='^SOKE_[A-Z0-9_]+$'
|
|
12
|
+
fail=0
|
|
13
|
+
|
|
14
|
+
for yaml in $(find extension/vendorplugin/builtin -name vendor.yaml 2>/dev/null); do
|
|
15
|
+
# 抽出所有 ${...} 占位符内层内容(去掉默认值部分)
|
|
16
|
+
vars=$(grep -oE '\$\{[^{}]+\}' "$yaml" \
|
|
17
|
+
| sed -E 's/^\$\{([^:}]+)(:-[^}]*)?\}$/\1/' \
|
|
18
|
+
| sort -u)
|
|
19
|
+
for v in $vars; do
|
|
20
|
+
if ! echo "$v" | grep -qE "$WHITELIST_RE"; then
|
|
21
|
+
echo "[FAIL] $yaml 引用了非白名单 env: $v"
|
|
22
|
+
fail=1
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
if [ "$fail" -ne 0 ]; then
|
|
28
|
+
echo
|
|
29
|
+
echo "白名单规则:^SOKE_[A-Z0-9_]+\$(见 extension/vendorplugin/env_whitelist.go)"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
echo "[OK] 所有 vendor.yaml 内 \${VAR} 占位符均符合 SOKE_* 白名单。"
|
package/scripts/install.js
CHANGED
|
@@ -107,6 +107,46 @@ function detectSkillNames(packagedSkillsDir) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function removeManagedSkillDir(targetDir, skillName) {
|
|
111
|
+
const dest = path.join(targetDir, skillName);
|
|
112
|
+
if (!fs.existsSync(dest)) return false;
|
|
113
|
+
try {
|
|
114
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
115
|
+
return true;
|
|
116
|
+
} catch (_) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pruneManagedSkillDirs(targetDir, expectedSkillNames) {
|
|
122
|
+
if (!fs.existsSync(targetDir)) return [];
|
|
123
|
+
try {
|
|
124
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
125
|
+
const removed = [];
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isDirectory() || !entry.name.startsWith('soke-')) continue;
|
|
128
|
+
if (expectedSkillNames.includes(entry.name)) continue;
|
|
129
|
+
if (removeManagedSkillDir(targetDir, entry.name)) {
|
|
130
|
+
removed.push(entry.name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return removed;
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pruneRegistrySkills(registry, expectedSkillNames) {
|
|
140
|
+
if (!registry || !Array.isArray(registry.skills)) return 0;
|
|
141
|
+
const before = registry.skills.length;
|
|
142
|
+
registry.skills = registry.skills.filter((entry) => {
|
|
143
|
+
if (!entry || typeof entry.name !== 'string') return true;
|
|
144
|
+
if (!entry.name.startsWith('soke-')) return true;
|
|
145
|
+
return expectedSkillNames.includes(entry.name);
|
|
146
|
+
});
|
|
147
|
+
return before - registry.skills.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
110
150
|
function syncSkillsToSokeclawWorkspace() {
|
|
111
151
|
const packageRoot = path.join(__dirname, '..');
|
|
112
152
|
const packagedSkillsDir = path.join(packageRoot, 'skills');
|
|
@@ -118,6 +158,7 @@ function syncSkillsToSokeclawWorkspace() {
|
|
|
118
158
|
for (const targetDir of targetDirs) {
|
|
119
159
|
try {
|
|
120
160
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
161
|
+
pruneManagedSkillDirs(targetDir, skillNames);
|
|
121
162
|
for (const skillName of skillNames) {
|
|
122
163
|
const src = path.join(packagedSkillsDir, skillName);
|
|
123
164
|
const dest = path.join(targetDir, skillName);
|
|
@@ -263,6 +304,9 @@ function syncSkillsToWorkclawRegistry() {
|
|
|
263
304
|
if (fs.existsSync(src)) copyDirRecursive(src, dest);
|
|
264
305
|
}
|
|
265
306
|
|
|
307
|
+
pruneManagedSkillDirs(workclawSkillInstallDir, skillNames);
|
|
308
|
+
pruneRegistrySkills(registry, skillNames);
|
|
309
|
+
|
|
266
310
|
// 自动注册所有 skills
|
|
267
311
|
for (const skillName of skillNames) {
|
|
268
312
|
const skillDir = path.join(packagedSkillsDir, skillName);
|
|
@@ -418,29 +462,19 @@ const archMap = {
|
|
|
418
462
|
const mappedPlatform = platformMap[platform];
|
|
419
463
|
const mappedArch = archMap[arch];
|
|
420
464
|
|
|
421
|
-
if (!mappedPlatform || !mappedArch) {
|
|
422
|
-
console.error(`不支持的平台: ${platform}-${arch}`);
|
|
423
|
-
console.error('支持的平台: darwin-x64, darwin-arm64, linux-x64, windows-x64');
|
|
424
|
-
process.exit(1);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
465
|
const binaryName = platform === 'win32' ? 'soke-cli.exe' : 'soke-cli';
|
|
428
|
-
const binaryFileName =
|
|
466
|
+
const binaryFileName = mappedPlatform && mappedArch
|
|
467
|
+
? `soke-cli-${mappedPlatform}-${mappedArch}${platform === 'win32' ? '.exe' : ''}`
|
|
468
|
+
: '';
|
|
429
469
|
|
|
430
470
|
// GitHub Releases 下载地址
|
|
431
|
-
const downloadURL =
|
|
471
|
+
const downloadURL = binaryFileName
|
|
472
|
+
? `https://github.com/liuchenlong1111/soke-cli/releases/download/v${version}/${binaryFileName}`
|
|
473
|
+
: '';
|
|
432
474
|
|
|
433
475
|
const binDir = path.join(__dirname, '..', 'bin');
|
|
434
476
|
const binaryPath = path.join(binDir, binaryName);
|
|
435
477
|
|
|
436
|
-
console.log(`正在为 ${mappedPlatform}-${mappedArch} 下载 soke-cli v${version}...`);
|
|
437
|
-
console.log(`下载地址: ${downloadURL}`);
|
|
438
|
-
|
|
439
|
-
// 创建 bin 目录
|
|
440
|
-
if (!fs.existsSync(binDir)) {
|
|
441
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
478
|
// 下载文件
|
|
445
479
|
function downloadFile(url, dest) {
|
|
446
480
|
return new Promise((resolve, reject) => {
|
|
@@ -491,43 +525,67 @@ function downloadFile(url, dest) {
|
|
|
491
525
|
}
|
|
492
526
|
|
|
493
527
|
// 执行下载
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
528
|
+
if (require.main === module) {
|
|
529
|
+
if (!mappedPlatform || !mappedArch) {
|
|
530
|
+
console.error(`不支持的平台: ${platform}-${arch}`);
|
|
531
|
+
console.error('支持的平台: darwin-x64, darwin-arm64, linux-x64, windows-x64');
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
console.log(`正在为 ${mappedPlatform}-${mappedArch} 下载 soke-cli v${version}...`);
|
|
536
|
+
console.log(`下载地址: ${downloadURL}`);
|
|
537
|
+
|
|
538
|
+
if (!fs.existsSync(binDir)) {
|
|
539
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
downloadFile(downloadURL, binaryPath)
|
|
543
|
+
.then(() => {
|
|
544
|
+
// 设置可执行权限(非 Windows 平台)
|
|
545
|
+
if (platform !== 'win32') {
|
|
546
|
+
try {
|
|
547
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
548
|
+
console.log('已设置可执行权限');
|
|
549
|
+
} catch (err) {
|
|
550
|
+
console.error('设置可执行权限失败:', err.message);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
504
553
|
}
|
|
505
|
-
}
|
|
506
554
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
555
|
+
try {
|
|
556
|
+
syncSkillsToSokeclawWorkspace();
|
|
557
|
+
} catch (_) {}
|
|
510
558
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
559
|
+
try {
|
|
560
|
+
syncSkillsToWorkclawRegistry();
|
|
561
|
+
} catch (_) {}
|
|
514
562
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
563
|
+
return maybeAssistGuiPath();
|
|
564
|
+
})
|
|
565
|
+
.then(() => {
|
|
566
|
+
console.log('soke-cli 安装成功!');
|
|
567
|
+
console.log(`二进制文件位置: ${binaryPath}`);
|
|
568
|
+
console.log('\n使用方法:');
|
|
569
|
+
console.log(' soke-cli --help');
|
|
570
|
+
console.log(' soke-cli config init');
|
|
571
|
+
console.log(' soke-cli auth login');
|
|
572
|
+
})
|
|
573
|
+
.catch((err) => {
|
|
574
|
+
console.error('\n安装失败:', err.message);
|
|
575
|
+
console.error('\n可能的原因:');
|
|
576
|
+
console.error('1. 网络连接问题');
|
|
577
|
+
console.error('2. GitHub Releases 中不存在该版本的二进制文件');
|
|
578
|
+
console.error('3. 不支持当前平台');
|
|
579
|
+
console.error('\n请访问 https://github.com/sokeai/soke-cli/releases 手动下载');
|
|
580
|
+
process.exit(1);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
module.exports = {
|
|
585
|
+
detectSkillNames,
|
|
586
|
+
parseSkillMetadata,
|
|
587
|
+
pruneManagedSkillDirs,
|
|
588
|
+
pruneRegistrySkills,
|
|
589
|
+
syncSkillsToSokeclawWorkspace,
|
|
590
|
+
syncSkillsToWorkclawRegistry
|
|
591
|
+
};
|
package/scripts/local-test.js
CHANGED
|
@@ -104,6 +104,46 @@ function detectSkillNames(packagedSkillsDir) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function removeManagedSkillDir(targetDir, skillName) {
|
|
108
|
+
const skillDir = path.join(targetDir, skillName);
|
|
109
|
+
if (!fs.existsSync(skillDir)) return false;
|
|
110
|
+
try {
|
|
111
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
112
|
+
return true;
|
|
113
|
+
} catch (_) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function pruneManagedSkillDirs(targetDir, expectedSkillNames) {
|
|
119
|
+
if (!fs.existsSync(targetDir)) return [];
|
|
120
|
+
try {
|
|
121
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
122
|
+
const removed = [];
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (!entry.isDirectory() || !entry.name.startsWith('soke-')) continue;
|
|
125
|
+
if (expectedSkillNames.includes(entry.name)) continue;
|
|
126
|
+
if (removeManagedSkillDir(targetDir, entry.name)) {
|
|
127
|
+
removed.push(entry.name);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return removed;
|
|
131
|
+
} catch (_) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pruneRegistrySkills(registry, expectedSkillNames) {
|
|
137
|
+
if (!registry || !Array.isArray(registry.skills)) return 0;
|
|
138
|
+
const before = registry.skills.length;
|
|
139
|
+
registry.skills = registry.skills.filter((entry) => {
|
|
140
|
+
if (!entry || typeof entry.name !== 'string') return true;
|
|
141
|
+
if (!entry.name.startsWith('soke-')) return true;
|
|
142
|
+
return expectedSkillNames.includes(entry.name);
|
|
143
|
+
});
|
|
144
|
+
return before - registry.skills.length;
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
/**
|
|
108
148
|
* 从 SKILL.md 解析元数据
|
|
109
149
|
*/
|
|
@@ -226,6 +266,7 @@ function detectLocalAgentDirs() {
|
|
|
226
266
|
* 更新 workclaw registry.json
|
|
227
267
|
*/
|
|
228
268
|
function updateWorkclawRegistry(registryPath, skillName, metadata, skillInstallPath) {
|
|
269
|
+
const safeMetadata = metadata && typeof metadata === 'object' ? metadata : {};
|
|
229
270
|
let registry;
|
|
230
271
|
|
|
231
272
|
try {
|
|
@@ -238,11 +279,11 @@ function updateWorkclawRegistry(registryPath, skillName, metadata, skillInstallP
|
|
|
238
279
|
if (!registry.migrations) registry.migrations = {};
|
|
239
280
|
if (!Array.isArray(registry.skills)) registry.skills = [];
|
|
240
281
|
|
|
241
|
-
const displayName =
|
|
242
|
-
const description =
|
|
243
|
-
const version =
|
|
282
|
+
const displayName = safeMetadata.summary || safeMetadata.name || skillName;
|
|
283
|
+
const description = safeMetadata.description || `${displayName} - 授客AI CLI工具`;
|
|
284
|
+
const version = safeMetadata.version || '1.0.0';
|
|
244
285
|
const emoji = inferSkillEmoji(skillName);
|
|
245
|
-
const requires =
|
|
286
|
+
const requires = safeMetadata.bins ? { bins: safeMetadata.bins } : {};
|
|
246
287
|
|
|
247
288
|
const skillEntry = {
|
|
248
289
|
id: `skill:${skillName}`,
|
|
@@ -323,6 +364,26 @@ function distributeSkill(skillName, packagedSkillsDir, targetDirs) {
|
|
|
323
364
|
return successCount > 0;
|
|
324
365
|
}
|
|
325
366
|
|
|
367
|
+
function reconcileTarget(target, expectedSkillNames) {
|
|
368
|
+
const removedDirs = pruneManagedSkillDirs(target.path, expectedSkillNames);
|
|
369
|
+
if (removedDirs.length > 0) {
|
|
370
|
+
logInfo(` 清理过期 skills: ${removedDirs.join(', ')}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (target.type === 'workclaw' && target.registryPath && fs.existsSync(target.registryPath)) {
|
|
374
|
+
try {
|
|
375
|
+
const registry = JSON.parse(fs.readFileSync(target.registryPath, 'utf8'));
|
|
376
|
+
const removedEntries = pruneRegistrySkills(registry, expectedSkillNames);
|
|
377
|
+
if (removedEntries > 0) {
|
|
378
|
+
fs.writeFileSync(target.registryPath, JSON.stringify(registry, null, 2));
|
|
379
|
+
logInfo(` 清理 registry 过期条目: ${removedEntries}`);
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
logError(` ✗ 清理 registry 失败: ${err.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
326
387
|
/**
|
|
327
388
|
* 清理所有本地环境中的 skills
|
|
328
389
|
*/
|
|
@@ -455,6 +516,10 @@ function main() {
|
|
|
455
516
|
}
|
|
456
517
|
}
|
|
457
518
|
|
|
519
|
+
for (const target of targetDirs) {
|
|
520
|
+
reconcileTarget(target, allSkills);
|
|
521
|
+
}
|
|
522
|
+
|
|
458
523
|
// 总结
|
|
459
524
|
logSection('分发完成');
|
|
460
525
|
|
|
@@ -493,11 +558,21 @@ function main() {
|
|
|
493
558
|
console.log('');
|
|
494
559
|
}
|
|
495
560
|
|
|
561
|
+
module.exports = {
|
|
562
|
+
detectSkillNames,
|
|
563
|
+
parseSkillMetadata,
|
|
564
|
+
pruneManagedSkillDirs,
|
|
565
|
+
pruneRegistrySkills,
|
|
566
|
+
updateWorkclawRegistry
|
|
567
|
+
};
|
|
568
|
+
|
|
496
569
|
// 运行
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
570
|
+
if (require.main === module) {
|
|
571
|
+
try {
|
|
572
|
+
main();
|
|
573
|
+
} catch (err) {
|
|
574
|
+
logError(`发生错误: ${err.message}`);
|
|
575
|
+
console.error(err);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
503
578
|
}
|
package/scripts/local-test.sh
CHANGED
|
@@ -109,59 +109,15 @@ echo ""
|
|
|
109
109
|
|
|
110
110
|
# 步骤3.5: 分发 Skills 到本地 Agent
|
|
111
111
|
echo -e "${YELLOW}[3.5/4] 分发 Skills 到本地 Agent...${NC}"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# 注入特殊环境变量或者直接调用 node 执行
|
|
115
|
-
# 我们只想要触发 syncSkillsToSokeclawWorkspace 和 syncSkillsToWorkclawRegistry
|
|
116
|
-
# 但原 install.js 会尝试下载二进制文件,所以我们新建一个临时脚本或修改调用方式
|
|
117
|
-
|
|
118
|
-
# 为了安全起见,这里直接使用 node 运行一小段脚本引入 install.js 中的逻辑
|
|
119
|
-
cat > ./scripts/sync-skills-local.js << 'EOF'
|
|
120
|
-
const fs = require('fs');
|
|
121
|
-
const path = require('path');
|
|
122
|
-
const installCode = fs.readFileSync(path.join(__dirname, 'install.js'), 'utf8');
|
|
123
|
-
|
|
124
|
-
// 提取需要的函数
|
|
125
|
-
const syncFn1Match = installCode.match(/function syncSkillsToSokeclawWorkspace\(\) \{[\s\S]*?\n\}/);
|
|
126
|
-
const syncFn2Match = installCode.match(/function syncSkillsToWorkclawRegistry\(\) \{[\s\S]*?\n\}/);
|
|
127
|
-
|
|
128
|
-
// 我们更推荐直接利用已经写好的 js 模块(如果它是通过 module.exports 导出的)
|
|
129
|
-
// 但因为 install.js 是直接执行的脚本,我们通过简单的 node 脚本手动拷贝
|
|
130
|
-
const os = require('os');
|
|
131
|
-
function copyDirRecursive(srcDir, destDir) {
|
|
132
|
-
if (!fs.existsSync(srcDir)) return;
|
|
133
|
-
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
134
|
-
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
135
|
-
for (const entry of entries) {
|
|
136
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
137
|
-
const destPath = path.join(destDir, entry.name);
|
|
138
|
-
if (entry.isDirectory()) { copyDirRecursive(srcPath, destPath); continue; }
|
|
139
|
-
fs.copyFileSync(srcPath, destPath);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const homeDir = os.homedir();
|
|
144
|
-
const defaultSokeclawDir = path.join(homeDir, '.sokeclaw', 'openai-agents', 'workspaces', 'main', 'skills');
|
|
145
|
-
const packageRoot = path.join(__dirname, '..');
|
|
146
|
-
const packagedSkillsDir = path.join(packageRoot, 'skills');
|
|
147
|
-
|
|
148
|
-
if (fs.existsSync(packagedSkillsDir)) {
|
|
149
|
-
const skillNames = fs.readdirSync(packagedSkillsDir).filter(n => n.startsWith('soke-'));
|
|
150
|
-
if (fs.existsSync(defaultSokeclawDir)) {
|
|
151
|
-
for (const skillName of skillNames) {
|
|
152
|
-
const src = path.join(packagedSkillsDir, skillName);
|
|
153
|
-
const dest = path.join(defaultSokeclawDir, skillName);
|
|
154
|
-
copyDirRecursive(src, dest);
|
|
155
|
-
console.log(` ✓ 同步 ${skillName} 到 ${dest}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
EOF
|
|
160
|
-
node ./scripts/sync-skills-local.js
|
|
161
|
-
rm ./scripts/sync-skills-local.js
|
|
112
|
+
echo -e " 执行 scripts/local-test.js 同步并清理本地 Agent Skills..."
|
|
113
|
+
if node ./scripts/local-test.js; then
|
|
162
114
|
echo -e "${GREEN} ✓ Skills 同步完成${NC}"
|
|
115
|
+
if [ "$SKIP_GLOBAL" = true ]; then
|
|
116
|
+
echo -e "${YELLOW} 提示: 已跳过全局 CLI 覆盖,但本地 Agent Skills 已按当前仓库状态完成同步${NC}"
|
|
117
|
+
fi
|
|
163
118
|
else
|
|
164
|
-
echo -e "${
|
|
119
|
+
echo -e "${RED} ✗ Skills 同步失败${NC}"
|
|
120
|
+
exit 1
|
|
165
121
|
fi
|
|
166
122
|
echo ""
|
|
167
123
|
|
|
@@ -222,4 +178,3 @@ echo ""
|
|
|
222
178
|
echo -e "${GREEN}========================================${NC}"
|
|
223
179
|
echo -e "${GREEN} 本地测试完成! ✓${NC}"
|
|
224
180
|
echo -e "${GREEN}========================================${NC}"
|
|
225
|
-
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# mcp-stdout-scan.sh — openspec change make-soke-cli-mcp-first §10.3
|
|
3
|
+
#
|
|
4
|
+
# 静态扫描 internal/mcp/ / extension/vendor/builtin/ / internal/courseimport/ 三个
|
|
5
|
+
# 与 mcp stdio 帧纯净性相关的代码树,拦下任何会污染 stdout JSON-RPC 帧的调用:
|
|
6
|
+
# - fmt.Println / fmt.Printf / fmt.Print
|
|
7
|
+
# - fmt.Fprintln / Fprintf / Fprint to os.Stdout
|
|
8
|
+
# - os.Stdout.Write*
|
|
9
|
+
# - 顶层 print( / println( 内建函数
|
|
10
|
+
#
|
|
11
|
+
# 排除 _test.go (测试代码允许写 stdout);
|
|
12
|
+
# 通过 sed 把 `// ...` 注释剥掉再 grep,避免文档/注释里提到 fmt.Println 触发 false positive。
|
|
13
|
+
#
|
|
14
|
+
# 用法: bash scripts/mcp-stdout-scan.sh
|
|
15
|
+
# 退出码: 0 = 干净;1 = 有命中(并打到 stderr)。
|
|
16
|
+
|
|
17
|
+
set -e
|
|
18
|
+
|
|
19
|
+
ROOTS="internal/mcp extension/vendor/builtin internal/courseimport"
|
|
20
|
+
PATTERN='fmt\.(Println|Printf|Print\()|fmt\.(Fprintln|Fprintf|Fprint)\(os\.Stdout|os\.Stdout\.Write|^[[:space:]]*(print|println)\('
|
|
21
|
+
|
|
22
|
+
have_hits=0
|
|
23
|
+
for d in $ROOTS; do
|
|
24
|
+
if [ ! -d "$d" ]; then
|
|
25
|
+
echo "skip 不存在的目录 $d"
|
|
26
|
+
continue
|
|
27
|
+
fi
|
|
28
|
+
files=$(find "$d" -name '*.go' ! -name '*_test.go' 2>/dev/null)
|
|
29
|
+
for f in $files; do
|
|
30
|
+
# 把行号前缀附在文件内容上, 再用 sed 剥 // 注释 (单行注释一律剥), 再 grep
|
|
31
|
+
hits=$(awk '{ printf "%s:%d:%s\n", FILENAME, NR, $0 }' "$f" \
|
|
32
|
+
| sed 's_//.*$__' \
|
|
33
|
+
| grep -E "$PATTERN" \
|
|
34
|
+
|| true)
|
|
35
|
+
if [ -n "$hits" ]; then
|
|
36
|
+
echo "❌ $f 检测到 stdout-write 调用 (会破坏 mcp stdio JSON-RPC 帧):" >&2
|
|
37
|
+
echo "$hits" >&2
|
|
38
|
+
have_hits=1
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
if [ "$have_hits" -eq 1 ]; then
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
echo "✓ stdout 纯净性扫描通过"
|
package/scripts/push.sh
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 回归 auth 命令体感 — task 8.1
|
|
3
|
+
#
|
|
4
|
+
# 串行跑:
|
|
5
|
+
# 1. auth status --json (预期未登录)
|
|
6
|
+
# 2. auth login (人工:浏览器跳转 + 真账号授权)
|
|
7
|
+
# 3. auth status --json (预期已登录)
|
|
8
|
+
# 4. auth logout
|
|
9
|
+
# 5. auth status --json (预期未登录)
|
|
10
|
+
#
|
|
11
|
+
# 用法:
|
|
12
|
+
# SOKE_CLIENT_ID=... SOKE_CLIENT_SECRET=... bash scripts/regress-auth.sh
|
|
13
|
+
# 首次运行前 build:make install 或 go build -o bin/soke-cli main.go
|
|
14
|
+
#
|
|
15
|
+
# 输出建议用 tee 保存,事后人工对比 baseline:
|
|
16
|
+
# bash scripts/regress-auth.sh 2>&1 | tee /tmp/regress-after.txt
|
|
17
|
+
# diff /tmp/regress-before.txt /tmp/regress-after.txt
|
|
18
|
+
|
|
19
|
+
set -u
|
|
20
|
+
|
|
21
|
+
BIN="${SOKE_CLI_BIN:-./bin/soke-cli}"
|
|
22
|
+
if [ ! -x "$BIN" ]; then
|
|
23
|
+
BIN="$(command -v soke-cli 2>/dev/null || echo "$BIN")"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if [ ! -x "$BIN" ]; then
|
|
27
|
+
echo "[FATAL] 找不到 soke-cli 二进制(尝试 SOKE_CLI_BIN env 指定路径)" >&2
|
|
28
|
+
exit 2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
run_step() {
|
|
32
|
+
local label="$1"; shift
|
|
33
|
+
echo
|
|
34
|
+
echo "==== [$label] $* ===="
|
|
35
|
+
"$@"
|
|
36
|
+
local rc=$?
|
|
37
|
+
echo "==== [$label] exit=$rc ===="
|
|
38
|
+
return $rc
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# baseline 时间戳 mask:让 diff 忽略 expires_at / iat / nbf / exp 等时间字段。
|
|
42
|
+
mask_timestamps() {
|
|
43
|
+
sed -E 's/("(expires_at|iat|nbf|exp|access_token|refresh_token)":\s*)"[^"]*"/\1"<masked>"/g'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
run_step "1.before-status" "$BIN" auth status --json | mask_timestamps
|
|
47
|
+
echo
|
|
48
|
+
echo "请在浏览器完成授权后按回车继续…"
|
|
49
|
+
run_step "2.login" "$BIN" auth login
|
|
50
|
+
read -r _
|
|
51
|
+
run_step "3.after-login-status" "$BIN" auth status --json | mask_timestamps
|
|
52
|
+
run_step "4.logout" "$BIN" auth logout
|
|
53
|
+
run_step "5.final-status" "$BIN" auth status --json | mask_timestamps
|
|
54
|
+
|
|
55
|
+
echo
|
|
56
|
+
echo "[done] 完成。请人工对比 baseline diff(/tmp/regress-before.txt)。"
|