@sokeai/cli 1.0.8 → 1.0.13

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.8",
3
+ "version": "1.0.13",
4
4
  "description": "授客AI官方CLI工具 - 支持AI Agent Skills",
5
5
  "bin": {
6
6
  "soke-cli": "scripts/run.js"
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const fs = require('fs');
4
4
  const https = require('https');
5
5
  const { execSync } = require('child_process');
6
+ const readline = require('readline');
6
7
 
7
8
  const platform = os.platform();
8
9
  const arch = os.arch();
@@ -88,6 +89,227 @@ function syncSkillsToSokeclawWorkspace() {
88
89
  }
89
90
  }
90
91
 
92
+ function upsertSkillRegistryEntry(registry, entry) {
93
+ if (!registry || typeof registry !== 'object') return;
94
+ if (!Array.isArray(registry.skills)) registry.skills = [];
95
+
96
+ const idx = registry.skills.findIndex((s) => s && s.id === entry.id);
97
+ if (idx >= 0) {
98
+ registry.skills[idx] = { ...registry.skills[idx], ...entry };
99
+ return;
100
+ }
101
+
102
+ registry.skills.push(entry);
103
+ }
104
+
105
+ function syncSkillsToWorkclawRegistry() {
106
+ const homeDir = os.homedir();
107
+ const workclawRootDir = path.join(homeDir, '.workclaw');
108
+ const workclawSkillsDir = path.join(workclawRootDir, 'skills');
109
+ const registryPath = path.join(workclawSkillsDir, 'registry.json');
110
+
111
+ if (!fs.existsSync(workclawRootDir)) return;
112
+ try {
113
+ fs.mkdirSync(workclawSkillsDir, { recursive: true });
114
+ } catch (_) {
115
+ return;
116
+ }
117
+
118
+ const packageRoot = path.join(__dirname, '..');
119
+ const packagedSkillsDir = path.join(packageRoot, 'skills');
120
+ if (!fs.existsSync(packagedSkillsDir)) return;
121
+
122
+ let registry;
123
+ try {
124
+ registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
125
+ } catch (_) {
126
+ registry = { version: 1, migrations: {}, skills: [] };
127
+ }
128
+
129
+ if (registry.version == null) registry.version = 1;
130
+ if (!registry.migrations) registry.migrations = {};
131
+
132
+ const existingSkills = Array.isArray(registry.skills) ? registry.skills : [];
133
+ const existingInstallPaths = existingSkills
134
+ .map((s) => s?.install?.path)
135
+ .filter((p) => typeof p === 'string');
136
+
137
+ const workclawSkillInstallDir = workclawSkillsDir;
138
+
139
+ const defaultSourceType =
140
+ typeof existingSkills[0]?.source?.type === 'string' && existingSkills[0]?.source?.type
141
+ ? existingSkills[0].source.type
142
+ : 'local';
143
+
144
+ const skillNames = ['soke-shared', 'soke-exam'];
145
+ for (const skillName of skillNames) {
146
+ const src = path.join(packagedSkillsDir, skillName);
147
+ const dest = path.join(workclawSkillInstallDir, skillName);
148
+ if (fs.existsSync(src)) copyDirRecursive(src, dest);
149
+ }
150
+
151
+ upsertSkillRegistryEntry(registry, {
152
+ id: 'skill:soke-exam',
153
+ name: 'soke-exam',
154
+ displayName: '授客考试管理',
155
+ description: '授客考试管理:查询考试、考试分类、考试用户成绩、考试详情。',
156
+ source: { type: defaultSourceType, slug: '', url: '' },
157
+ install: {
158
+ path: path.join(workclawSkillInstallDir, 'soke-exam'),
159
+ installedAt: '',
160
+ updatedAt: '',
161
+ version: '1.0.0'
162
+ },
163
+ state: { enabled: true, health: 'ok', lastError: '' },
164
+ runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
165
+ security: { riskLevel: 'normal', requiresApproval: false },
166
+ metadata: { emoji: '📝', homepage: '', requires: { bins: ['soke-cli'] } }
167
+ });
168
+
169
+ upsertSkillRegistryEntry(registry, {
170
+ id: 'skill:soke-shared',
171
+ name: 'soke-shared',
172
+ displayName: 'soke-shared 共享规则',
173
+ description: '授客CLI共享基础:配置、登录、权限管理、错误处理、安全规则。',
174
+ source: { type: defaultSourceType, slug: '', url: '' },
175
+ install: {
176
+ path: path.join(workclawSkillInstallDir, 'soke-shared'),
177
+ installedAt: '',
178
+ updatedAt: '',
179
+ version: '1.0.0'
180
+ },
181
+ state: { enabled: true, health: 'ok', lastError: '' },
182
+ runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
183
+ security: { riskLevel: 'normal', requiresApproval: false },
184
+ metadata: { emoji: '🔧', homepage: '', requires: {} }
185
+ });
186
+
187
+ try {
188
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
189
+ } catch (_) {}
190
+ }
191
+
192
+ function isLikelyInteractiveInstall() {
193
+ if (!process.stdin.isTTY) return false;
194
+ if (!process.stdout.isTTY) return false;
195
+ if (process.env.CI) return false;
196
+ if (process.env.npm_config_yes === 'true') return false;
197
+ return true;
198
+ }
199
+
200
+ function promptYesNo(message) {
201
+ return new Promise((resolve) => {
202
+ const rl = readline.createInterface({
203
+ input: process.stdin,
204
+ output: process.stdout
205
+ });
206
+
207
+ rl.question(message, (answer) => {
208
+ rl.close();
209
+ const normalized = String(answer || '').trim().toLowerCase();
210
+ resolve(normalized === 'y' || normalized === 'yes');
211
+ });
212
+ });
213
+ }
214
+
215
+ function detectNpmGlobalBinDir() {
216
+ const prefix = process.env.npm_config_prefix;
217
+ if (typeof prefix === 'string' && prefix.length > 0) {
218
+ const binDir = platform === 'win32' ? prefix : path.join(prefix, 'bin');
219
+ return binDir;
220
+ }
221
+ return null;
222
+ }
223
+
224
+ function detectSokeCliShimPath() {
225
+ const binDir = detectNpmGlobalBinDir();
226
+ if (!binDir) return null;
227
+
228
+ const candidates =
229
+ platform === 'win32'
230
+ ? [path.join(binDir, 'soke-cli.cmd'), path.join(binDir, 'soke-cli.exe')]
231
+ : [path.join(binDir, 'soke-cli')];
232
+
233
+ for (const candidate of candidates) {
234
+ if (fs.existsSync(candidate)) return candidate;
235
+ }
236
+ return null;
237
+ }
238
+
239
+ function getPreferredLinkTargetPaths() {
240
+ if (platform === 'win32') return [];
241
+ return ['/usr/local/bin/soke-cli', '/opt/homebrew/bin/soke-cli'];
242
+ }
243
+
244
+ function ensureSymlink(targetPath, sourcePath) {
245
+ try {
246
+ const existing = fs.lstatSync(targetPath);
247
+ if (existing.isSymbolicLink()) {
248
+ const currentTarget = fs.readlinkSync(targetPath);
249
+ if (currentTarget === sourcePath) return { ok: true, changed: false };
250
+ fs.unlinkSync(targetPath);
251
+ } else {
252
+ return { ok: false, changed: false, reason: 'exists_non_symlink' };
253
+ }
254
+ } catch (_) {}
255
+
256
+ try {
257
+ fs.symlinkSync(sourcePath, targetPath);
258
+ return { ok: true, changed: true };
259
+ } catch (err) {
260
+ return { ok: false, changed: false, reason: err && err.message ? err.message : 'failed' };
261
+ }
262
+ }
263
+
264
+ async function maybeAssistGuiPath() {
265
+ if (!isLikelyInteractiveInstall()) return;
266
+
267
+ const shimPath = detectSokeCliShimPath();
268
+ if (!shimPath) return;
269
+
270
+ const targets = getPreferredLinkTargetPaths();
271
+ if (targets.length === 0) return;
272
+
273
+ let selectedTarget = null;
274
+ for (const t of targets) {
275
+ const dir = path.dirname(t);
276
+ if (fs.existsSync(dir)) {
277
+ selectedTarget = t;
278
+ break;
279
+ }
280
+ }
281
+ if (!selectedTarget) selectedTarget = targets[0];
282
+
283
+ const dir = path.dirname(selectedTarget);
284
+ let dirWritable = false;
285
+ try {
286
+ fs.accessSync(dir, fs.constants.W_OK);
287
+ dirWritable = true;
288
+ } catch (_) {}
289
+
290
+ const question = dirWritable
291
+ ? `检测到你可能在 sokeclaw(GUI)里遇到 “command not found: soke-cli”。是否创建链接 ${selectedTarget} 指向 ${shimPath} 以便 GUI 可直接找到?(y/N) `
292
+ : `检测到你可能在 sokeclaw(GUI)里遇到 “command not found: soke-cli”。是否输出一条需要 sudo 的命令来创建链接 ${selectedTarget} 指向 ${shimPath}?(y/N) `;
293
+
294
+ const ok = await promptYesNo(question);
295
+ if (!ok) return;
296
+
297
+ if (dirWritable) {
298
+ const res = ensureSymlink(selectedTarget, shimPath);
299
+ if (res.ok) {
300
+ console.log(`已配置:${selectedTarget} -> ${shimPath}`);
301
+ } else if (res.reason === 'exists_non_symlink') {
302
+ console.log(`跳过:${selectedTarget} 已存在且不是软链接。`);
303
+ } else {
304
+ console.log(`创建软链接失败:${res.reason}`);
305
+ }
306
+ return;
307
+ }
308
+
309
+ console.log(`请执行以下命令完成配置(需要 sudo):`);
310
+ console.log(`sudo ln -sf "${shimPath}" "${selectedTarget}"`);
311
+ }
312
+
91
313
  // 平台映射
92
314
  const platformMap = {
93
315
  'darwin': 'darwin',
@@ -193,6 +415,13 @@ downloadFile(downloadURL, binaryPath)
193
415
  syncSkillsToSokeclawWorkspace();
194
416
  } catch (_) {}
195
417
 
418
+ try {
419
+ syncSkillsToWorkclawRegistry();
420
+ } catch (_) {}
421
+
422
+ return maybeAssistGuiPath();
423
+ })
424
+ .then(() => {
196
425
  console.log('soke-cli 安装成功!');
197
426
  console.log(`二进制文件位置: ${binaryPath}`);
198
427
  console.log('\n使用方法:');
package/scripts/run.js CHANGED
@@ -1,9 +1,88 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn } = require('child_process');
3
+ const { spawn, execSync } = require('child_process');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
 
7
+ // 特殊命令拦截:修复 sokeclaw 的 GUI PATH 问题
8
+ if (process.argv[2] === 'setup-gui-env') {
9
+ console.log('正在为 SokeClaw GUI 注入环境变量 PATH...');
10
+ try {
11
+ // 获取 node 和当前脚本的绝对路径目录,这通常也是 soke-cli 所在的 bin 目录
12
+ const nodeDir = path.dirname(process.execPath);
13
+ const cliDir = path.dirname(process.argv[1]);
14
+
15
+ let targetPath = '';
16
+ if (process.platform === 'darwin') {
17
+ // 提取现有的系统 PATH
18
+ const sysPath = execSync('sysctl -n getenv PATH 2>/dev/null || echo ""', { encoding: 'utf8' }).trim();
19
+ const currentPath = process.env.PATH || '';
20
+
21
+ // 合并并去重 PATH
22
+ const pathSet = new Set([nodeDir, cliDir, ...currentPath.split(':'), ...sysPath.split(':')].filter(Boolean));
23
+ targetPath = Array.from(pathSet).join(':');
24
+
25
+ console.log(`注入的 PATH: ${targetPath.substring(0, 100)}...`);
26
+ // launchctl setenv 在新版 macOS 中如果不在 login context 下可能提示 Not privileged,
27
+ // 我们改用软链到 GUI 默认 PATH /usr/local/bin 的方式来解决(如果不可写则提示 sudo)
28
+ const binSource = path.join(nodeDir, 'soke-cli');
29
+ const binTarget = '/usr/local/bin/soke-cli';
30
+
31
+ if (fs.existsSync(binSource)) {
32
+ try {
33
+ // 尝试创建软链
34
+ if (fs.existsSync(binTarget)) {
35
+ try { fs.unlinkSync(binTarget); } catch(e) {}
36
+ }
37
+ fs.symlinkSync(binSource, binTarget);
38
+ console.log(`✅ 成功创建软链: ${binTarget} -> ${binSource}`);
39
+ } catch (linkError) {
40
+ console.log(`\n⚠️ 权限不足,无法自动创建 /usr/local/bin 软链。`);
41
+ console.log(`请手动在终端执行以下命令(可能需要输入密码):`);
42
+ console.log(`\n sudo ln -sf "${binSource}" "${binTarget}"\n`);
43
+
44
+ // 如果这里没法创建,sokeclaw 可能还是找不到,提示一下
45
+ console.log(`执行上述命令后,再重新打开 SokeClaw 即可。`);
46
+ process.exit(0);
47
+ }
48
+ } else {
49
+ console.log(`⚠️ 未找到源文件: ${binSource}`);
50
+ }
51
+
52
+ console.log('尝试重启 SokeClaw / WorkClaw...');
53
+ try { execSync('killall SokeClaw 2>/dev/null'); } catch(e) {}
54
+ try { execSync('killall WorkClaw 2>/dev/null'); } catch(e) {}
55
+ try { execSync('killall Electron 2>/dev/null'); } catch(e) {}
56
+
57
+ // 等待进程退出
58
+ setTimeout(() => {
59
+ let launched = false;
60
+ if (fs.existsSync('/Applications/SokeClaw.app')) {
61
+ execSync('open "/Applications/SokeClaw.app"');
62
+ launched = true;
63
+ } else if (fs.existsSync('/Applications/WorkClaw.app')) {
64
+ execSync('open "/Applications/WorkClaw.app"');
65
+ launched = true;
66
+ }
67
+
68
+ if (launched) {
69
+ console.log('\n✅ 修复完成!SokeClaw 已重启。现在你应该可以在里面使用 /skill soke-exam 了。');
70
+ } else {
71
+ console.log('\n✅ 环境变量已注入,但未找到 SokeClaw 应用。请手动从“启动台(Launchpad)”或“访达(Finder)”重新打开它。');
72
+ }
73
+ process.exit(0);
74
+ }, 1500);
75
+ return; // 异步等待中
76
+ } else {
77
+ console.log('\n⚠️ 目前 setup-gui-env 仅支持 macOS 平台。Windows/Linux 用户请手动将 Node/npm bin 目录加入系统环境变量。');
78
+ process.exit(0);
79
+ }
80
+ } catch (error) {
81
+ console.error('修复过程中出现错误:', error.message);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
7
86
  // 获取二进制文件路径
8
87
  const platform = process.platform;
9
88
  const binaryName = platform === 'win32' ? 'soke-cli.exe' : 'soke-cli';
@@ -50,4 +129,4 @@ child.on('exit', (code, signal) => {
50
129
  child.on('error', (err) => {
51
130
  console.error('执行 soke-cli 时出错:', err.message);
52
131
  process.exit(1);
53
- });
132
+ });
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: soke-exam
3
+ summary: 授客考试管理(考试列表/分类/考试用户成绩/详情),通过 soke-cli 查询
3
4
  version: 1.0.0
4
5
  description: "授客考试管理:查询考试、考试用户和成绩。查询考试列表、考试分类、考试用户成绩、考试详情。当用户需要查询考试成绩、查看考试列表、查询考试用户信息、查看考试分类时使用。"
5
6
  metadata:
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: soke-shared
3
+ summary: 授客CLI共享基础(配置/登录/权限/错误处理/安全规则)
3
4
  version: 1.0.0
4
5
  description: "授客CLI共享基础:应用配置初始化、认证登录(auth login)、权限管理、错误处理、安全规则。当用户需要第一次配置(soke-cli config init)、使用登录授权(soke-cli auth login)、遇到权限不足、或首次使用soke-cli时触发。"
5
6
  ---