@sokeai/cli 1.0.34 → 1.0.39
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 +67 -16
- package/scripts/local-test.js +85 -10
- package/scripts/local-test.sh +7 -27
- package/scripts/mcp-stdout-scan.sh +46 -0
- package/scripts/push.sh +16 -25
- package/scripts/regress-auth.sh +56 -0
- package/scripts/regress-baseline.md +84 -0
- package/scripts/release.sh +7 -0
- package/scripts/test-auto-detect.js +48 -71
- package/scripts/test-local-reconcile.js +55 -0
- package/skills/soke-business-training-report/SKILL.md +1 -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) => {
|
|
@@ -492,6 +526,19 @@ function downloadFile(url, dest) {
|
|
|
492
526
|
|
|
493
527
|
// 执行下载
|
|
494
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
|
+
|
|
495
542
|
downloadFile(downloadURL, binaryPath)
|
|
496
543
|
.then(() => {
|
|
497
544
|
// 设置可执行权限(非 Windows 平台)
|
|
@@ -535,6 +582,10 @@ if (require.main === module) {
|
|
|
535
582
|
}
|
|
536
583
|
|
|
537
584
|
module.exports = {
|
|
585
|
+
detectSkillNames,
|
|
586
|
+
parseSkillMetadata,
|
|
587
|
+
pruneManagedSkillDirs,
|
|
588
|
+
pruneRegistrySkills,
|
|
538
589
|
syncSkillsToSokeclawWorkspace,
|
|
539
590
|
syncSkillsToWorkclawRegistry
|
|
540
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,34 +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
|
-
|
|
115
|
-
# 使用 node 调用 install.js 中导出的同步函数
|
|
116
|
-
cat > ./scripts/sync-skills-local.js << 'EOF'
|
|
117
|
-
const install = require('./install.js');
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
console.log(" 同步到 Sokeclaw Workspace...");
|
|
121
|
-
install.syncSkillsToSokeclawWorkspace();
|
|
122
|
-
console.log(" ✓ Sokeclaw Workspace 同步成功");
|
|
123
|
-
} catch (e) {
|
|
124
|
-
console.error(" ✗ Sokeclaw Workspace 同步失败:", e.message);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
console.log(" 同步到 Workclaw Registry...");
|
|
129
|
-
install.syncSkillsToWorkclawRegistry();
|
|
130
|
-
console.log(" ✓ Workclaw Registry 同步成功");
|
|
131
|
-
} catch (e) {
|
|
132
|
-
console.error(" ✗ Workclaw Registry 同步失败:", e.message);
|
|
133
|
-
}
|
|
134
|
-
EOF
|
|
135
|
-
node ./scripts/sync-skills-local.js
|
|
136
|
-
rm ./scripts/sync-skills-local.js
|
|
112
|
+
echo -e " 执行 scripts/local-test.js 同步并清理本地 Agent Skills..."
|
|
113
|
+
if node ./scripts/local-test.js; then
|
|
137
114
|
echo -e "${GREEN} ✓ Skills 同步完成${NC}"
|
|
115
|
+
if [ "$SKIP_GLOBAL" = true ]; then
|
|
116
|
+
echo -e "${YELLOW} 提示: 已跳过全局 CLI 覆盖,但本地 Agent Skills 已按当前仓库状态完成同步${NC}"
|
|
117
|
+
fi
|
|
138
118
|
else
|
|
139
|
-
echo -e "${
|
|
119
|
+
echo -e "${RED} ✗ Skills 同步失败${NC}"
|
|
120
|
+
exit 1
|
|
140
121
|
fi
|
|
141
122
|
echo ""
|
|
142
123
|
|
|
@@ -197,4 +178,3 @@ echo ""
|
|
|
197
178
|
echo -e "${GREEN}========================================${NC}"
|
|
198
179
|
echo -e "${GREEN} 本地测试完成! ✓${NC}"
|
|
199
180
|
echo -e "${GREEN}========================================${NC}"
|
|
200
|
-
|
|
@@ -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
|
@@ -191,33 +191,24 @@ git commit -m "$commit_info"
|
|
|
191
191
|
# 保存当前分支名(用于后续合并)
|
|
192
192
|
FEATURE_BRANCH=$CURRENT_BRANCH
|
|
193
193
|
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
git checkout master
|
|
194
|
+
# 目标主分支名称
|
|
195
|
+
TARGET_BRANCH="master"
|
|
197
196
|
|
|
198
|
-
#
|
|
199
|
-
print_info "
|
|
200
|
-
git
|
|
201
|
-
|
|
202
|
-
# 推送到 origin (Aliyun) 的 master 分支
|
|
203
|
-
print_info "推送到 Aliyun 远程仓库 (origin master)..."
|
|
204
|
-
git push origin master
|
|
197
|
+
# 切换到主分支
|
|
198
|
+
print_info "切换到 $TARGET_BRANCH 分支..."
|
|
199
|
+
git checkout $TARGET_BRANCH
|
|
205
200
|
|
|
206
|
-
#
|
|
207
|
-
print_info "
|
|
208
|
-
git
|
|
209
|
-
|
|
210
|
-
# 切换到 main 分支
|
|
211
|
-
print_info "切换到 main 分支..."
|
|
212
|
-
git checkout main
|
|
201
|
+
# 合并功能分支到主分支
|
|
202
|
+
print_info "合并 ${FEATURE_BRANCH} 到 $TARGET_BRANCH..."
|
|
203
|
+
git merge $FEATURE_BRANCH
|
|
213
204
|
|
|
214
|
-
#
|
|
215
|
-
print_info "
|
|
216
|
-
git
|
|
205
|
+
# 推送到 origin (Aliyun) 的主分支
|
|
206
|
+
print_info "推送到 Aliyun 远程仓库 (origin $TARGET_BRANCH)..."
|
|
207
|
+
git push origin $TARGET_BRANCH
|
|
217
208
|
|
|
218
|
-
# 推送到 github
|
|
219
|
-
print_info "推送到 GitHub 远程仓库 (github
|
|
220
|
-
git push github
|
|
209
|
+
# 推送到 github 的主分支
|
|
210
|
+
print_info "推送到 GitHub 远程仓库 (github $TARGET_BRANCH)..."
|
|
211
|
+
git push github $TARGET_BRANCH
|
|
221
212
|
|
|
222
213
|
# 切换回原分支
|
|
223
214
|
print_info "切换回原分支 ${FEATURE_BRANCH}..."
|
|
@@ -234,5 +225,5 @@ if [[ "$IS_RELEASE" == true ]]; then
|
|
|
234
225
|
echo " - 🎉 版本发布: ${GREEN}v${NEW_VERSION}${NC}"
|
|
235
226
|
fi
|
|
236
227
|
|
|
237
|
-
echo " - Aliyun (origin):
|
|
238
|
-
echo " - GitHub (github):
|
|
228
|
+
echo " - Aliyun (origin): $TARGET_BRANCH 分支已更新"
|
|
229
|
+
echo " - GitHub (github): $TARGET_BRANCH 分支已更新"
|
|
@@ -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)。"
|