@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sokeai/cli",
3
- "version": "1.0.37",
3
+ "version": "1.0.41",
4
4
  "description": "授客AI官方CLI工具 - 支持AI Agent Skills",
5
5
  "bin": {
6
6
  "soke-cli": "scripts/run.js"
@@ -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
- LDFLAGS="-s -w -X main.Version=${VERSION}"
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_* 白名单。"
@@ -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 = `soke-cli-${mappedPlatform}-${mappedArch}${platform === 'win32' ? '.exe' : ''}`;
466
+ const binaryFileName = mappedPlatform && mappedArch
467
+ ? `soke-cli-${mappedPlatform}-${mappedArch}${platform === 'win32' ? '.exe' : ''}`
468
+ : '';
429
469
 
430
470
  // GitHub Releases 下载地址
431
- const downloadURL = `https://github.com/liuchenlong1111/soke-cli/releases/download/v${version}/${binaryFileName}`;
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
- downloadFile(downloadURL, binaryPath)
495
- .then(() => {
496
- // 设置可执行权限(非 Windows 平台)
497
- if (platform !== 'win32') {
498
- try {
499
- fs.chmodSync(binaryPath, 0o755);
500
- console.log('已设置可执行权限');
501
- } catch (err) {
502
- console.error('设置可执行权限失败:', err.message);
503
- process.exit(1);
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
- try {
508
- syncSkillsToSokeclawWorkspace();
509
- } catch (_) {}
555
+ try {
556
+ syncSkillsToSokeclawWorkspace();
557
+ } catch (_) {}
510
558
 
511
- try {
512
- syncSkillsToWorkclawRegistry();
513
- } catch (_) {}
559
+ try {
560
+ syncSkillsToWorkclawRegistry();
561
+ } catch (_) {}
514
562
 
515
- return maybeAssistGuiPath();
516
- })
517
- .then(() => {
518
- console.log('soke-cli 安装成功!');
519
- console.log(`二进制文件位置: ${binaryPath}`);
520
- console.log('\n使用方法:');
521
- console.log(' soke-cli --help');
522
- console.log(' soke-cli config init');
523
- console.log(' soke-cli auth login');
524
- })
525
- .catch((err) => {
526
- console.error('\n安装失败:', err.message);
527
- console.error('\n可能的原因:');
528
- console.error('1. 网络连接问题');
529
- console.error('2. GitHub Releases 中不存在该版本的二进制文件');
530
- console.error('3. 不支持当前平台');
531
- console.error('\n请访问 https://github.com/sokeai/soke-cli/releases 手动下载');
532
- process.exit(1);
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
+ };
@@ -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 = metadata.summary || metadata.name || skillName;
242
- const description = metadata.description || `${displayName} - 授客AI CLI工具`;
243
- const version = metadata.version || '1.0.0';
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 = metadata.bins ? { bins: metadata.bins } : {};
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
- try {
498
- main();
499
- } catch (err) {
500
- logError(`发生错误: ${err.message}`);
501
- console.error(err);
502
- process.exit(1);
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
  }
@@ -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
- if [ "$SKIP_GLOBAL" = false ]; then
113
- echo -e " 执行 scripts/install.js 同步 Skills..."
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 "${YELLOW} 跳过全局安装,同时跳过 Skills 同步${NC}"
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
@@ -192,7 +192,7 @@ git commit -m "$commit_info"
192
192
  FEATURE_BRANCH=$CURRENT_BRANCH
193
193
 
194
194
  # 目标主分支名称
195
- TARGET_BRANCH="soke-cli"
195
+ TARGET_BRANCH="master"
196
196
 
197
197
  # 切换到主分支
198
198
  print_info "切换到 $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)。"