aisoulhub 1.0.11 → 1.1.4

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
@@ -1,35 +1,57 @@
1
1
  # AISoulHub CLI
2
2
 
3
- A command-line interface tool to easily install, update, and manage AI Soul configurations and skills for OpenClaw agents. It seamlessly integrates with [AISoulHub](https://aisoulhub.com) to provide a streamlined experience.
3
+ `aisoulhub` 是一个用于安装 aisoul 的命令行工具,可将 aisoul 安装到本地 OpenClaw workspace 中。
4
4
 
5
- ## Prerequisites
5
+ ## 环境要求
6
6
 
7
- Before using this tool, ensure you have the following installed:
7
+ - [Node.js](https://nodejs.org/) v16 或更高版本
8
+ - 已安装并可直接执行的 `openclaw`
9
+ - 可通过 `npx` 执行 `clawhub`
8
10
 
9
- - [Node.js](https://nodejs.org/) (v16 or higher recommended)
10
- - [OpenClaw](https://openclaw.ai/) CLI (`openclaw` command must be available in your PATH)
11
+ ## 使用方式
11
12
 
12
- ## Usage
13
+ ### 安装 aisoul
13
14
 
14
- You can run `aisoulhub` directly via `npx` without needing to install it globally.
15
+ ```bash
16
+ npx aisoulhub install <slug[@version]> --user <your@email.com>
17
+ ```
18
+
19
+ 示例:
20
+
21
+ ```bash
22
+ npx aisoulhub install helloworld --user your@gmail.com
23
+ ```
15
24
 
16
- ### 1. Install an AI Soul
25
+ 指定版本:
17
26
 
18
- Downloads and installs an AI Soul package into a new or existing OpenClaw Agent.
27
+ ```bash
28
+ npx aisoulhub install helloworld@1.0.2 --user your@gmail.com
29
+ ```
30
+
31
+ 使用自定义服务地址:
19
32
 
20
33
  ```bash
21
- npx aisoulhub install <ai_soul_id>
34
+ npx aisoulhub install helloworld --user your@gmail.com --base_url=https://aisoulhub.com
22
35
  ```
23
36
 
24
- ### 2. Update an AI Soul
37
+ 参数说明:
38
+
39
+ - `<slug[@version]>`:aisoul 标识,`@version` 可选
40
+ - `--user`:请使用你的注册邮箱
41
+ - `--base_url`:可选,自定义服务地址
42
+
43
+ 安装提示:
44
+
45
+ - 如果未指定版本,将自动安装该 aisoul 的最新版本
46
+ - 如果已存在同名 agent,请先执行 `openclaw agents delete <agentName>` 后再重新安装
25
47
 
26
- Updates an existing OpenClaw Agent with the latest AI Soul package configuration and skills.
48
+ ### 更新 aisoul
27
49
 
28
50
  ```bash
29
51
  npx aisoulhub update
30
52
  ```
31
53
 
32
- <br />
54
+ `update` 命令已预留,当前版本暂未实现。
33
55
 
34
56
  ## License
35
57
 
package/bin/aisoulhub.js CHANGED
@@ -3,6 +3,10 @@
3
3
  import { main } from '../src/index.js';
4
4
 
5
5
  main().catch(err => {
6
- console.error(err);
6
+ if (err && typeof err === 'object' && err.userFacing) {
7
+ console.error(err.message);
8
+ } else {
9
+ console.error(err);
10
+ }
7
11
  process.exit(1);
8
12
  });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "aisoulhub",
3
- "version": "1.0.11",
3
+ "version": "1.1.4",
4
4
  "description": "",
5
- "main": "index.js",
5
+ "main": "./src/index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
7
+ "test": "node ./test/install.test.js",
8
8
  "release": "npm version patch && npm publish --access public"
9
9
  },
10
10
  "files": [
@@ -22,7 +22,6 @@
22
22
  },
23
23
  "type": "module",
24
24
  "dependencies": {
25
- "@inquirer/prompts": "^8.3.2",
26
25
  "chalk": "^5.6.2",
27
26
  "clawhub": "^0.9.0",
28
27
  "commander": "^14.0.3",
package/src/index.js CHANGED
@@ -1,30 +1,601 @@
1
1
  import { Command } from 'commander';
2
- import { installCommand } from './commands/install.js';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
2
+ import crypto from 'node:crypto';
3
+ import { spawn } from 'node:child_process';
4
+ import fs from 'node:fs/promises';
5
+ import { createReadStream, createWriteStream } from 'node:fs';
6
+ import http from 'node:http';
7
+ import https from 'node:https';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { pipeline } from 'node:stream/promises';
11
+ import unzipper from 'unzipper';
6
12
 
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
13
+ export const DEFAULT_BASE_URL = 'https://aisoulhub.com';
14
+ const CLAWHUB_REGISTRY = 'https://cn.clawhub-mirror.com';
9
15
 
10
- export async function main() {
11
- const pkgPath = path.join(__dirname, '../package.json');
12
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
16
+ export class UserFacingError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'UserFacingError';
20
+ this.userFacing = true;
21
+ }
22
+ }
23
+
24
+ export function normalizeBaseUrl(baseUrl) {
25
+ return String(baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
26
+ }
27
+
28
+ export function buildRequestHeaders(user) {
29
+ const email = String(user || '').trim();
30
+ return email ? { 'X-USER-EMAIL': email } : {};
31
+ }
32
+
33
+ export function buildBaseUrlCandidates(baseUrl) {
34
+ const normalized = normalizeBaseUrl(baseUrl);
35
+ if (normalized.endsWith('/sc')) {
36
+ return [normalized];
37
+ }
38
+
39
+ return [normalized, `${normalized}/sc`];
40
+ }
41
+
42
+ export function parseSoulSpecifier(input) {
43
+ const value = String(input || '').trim();
44
+ const atIndex = value.lastIndexOf('@');
45
+
46
+ if (!value) {
47
+ throw new Error('请提供要安装的 aisoul 标识,例如 demo-soul 或 demo-soul@1.0.0');
48
+ }
49
+
50
+ if (atIndex <= 0) {
51
+ return { slug: value, version: null };
52
+ }
53
+
54
+ const slug = value.slice(0, atIndex).trim();
55
+ const version = value.slice(atIndex + 1).trim();
56
+
57
+ if (!slug || !version) {
58
+ throw new Error('aisoul 标识格式无效,请使用 slug 或 slug@version');
59
+ }
60
+
61
+ return { slug, version };
62
+ }
63
+
64
+ export function findExistingAgent(agents, agentName) {
65
+ if (!Array.isArray(agents)) {
66
+ return null;
67
+ }
68
+
69
+ return (
70
+ agents.find(agent => agent?.id === agentName || agent?.name === agentName) || null
71
+ );
72
+ }
73
+
74
+ export function normalizeSkillName(skill) {
75
+ const rawValue = String(skill || '').trim();
76
+ if (!rawValue) {
77
+ return '';
78
+ }
79
+
80
+ if (!rawValue.includes('/')) {
81
+ return rawValue;
82
+ }
83
+
84
+ return rawValue.split('/').filter(Boolean).pop() || '';
85
+ }
86
+
87
+ export function extractMd5Hash(content) {
88
+ const match = String(content || '').match(/\b([a-fA-F0-9]{32})\b/);
89
+ if (!match) {
90
+ throw new Error('无法解析远端返回的 MD5 校验码');
91
+ }
92
+
93
+ return match[1].toLowerCase();
94
+ }
95
+
96
+ export function buildMergedAgentsContent(existingContent, incomingContent) {
97
+ const incoming = String(incomingContent || '')
98
+ .replace(/^\uFEFF/, '')
99
+ .replace(/\s*$/, '');
100
+ if (!incoming) {
101
+ return String(existingContent || '');
102
+ }
103
+
104
+ const existing = String(existingContent || '');
105
+ const prefix = existing ? `${existing.replace(/\s*$/, '\n')}` : '';
106
+ return `${prefix}---\n${incoming}\n`;
107
+ }
108
+
109
+ async function fileExists(filePath) {
110
+ try {
111
+ await fs.access(filePath);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ function request(url, { headers = {}, redirectCount = 0 } = {}) {
119
+ if (redirectCount > 5) {
120
+ return Promise.reject(new Error(`请求重定向次数过多: ${url}`));
121
+ }
122
+
123
+ const parsedUrl = new URL(url);
124
+ const client = parsedUrl.protocol === 'https:' ? https : http;
125
+
126
+ return new Promise((resolve, reject) => {
127
+ const req = client.get(
128
+ parsedUrl,
129
+ {
130
+ headers,
131
+ },
132
+ res => {
133
+ const { statusCode = 0, headers } = res;
134
+
135
+ if ([301, 302, 303, 307, 308].includes(statusCode) && headers.location) {
136
+ res.resume();
137
+ const redirectedUrl = new URL(headers.location, parsedUrl).toString();
138
+ resolve(request(redirectedUrl, { headers, redirectCount: redirectCount + 1 }));
139
+ return;
140
+ }
141
+
142
+ if (statusCode >= 400) {
143
+ const chunks = [];
144
+ res.on('data', chunk => chunks.push(chunk));
145
+ res.on('end', () => {
146
+ const body = Buffer.concat(chunks).toString('utf8').trim();
147
+ const detail = body ? `: ${body}` : '';
148
+ reject(new Error(`请求失败 (${statusCode}) ${url}${detail}`));
149
+ });
150
+ return;
151
+ }
152
+
153
+ resolve(res);
154
+ }
155
+ );
156
+
157
+ req.on('error', reject);
158
+ });
159
+ }
160
+
161
+ async function fetchText(url, options = {}) {
162
+ const res = await request(url, options);
163
+ const chunks = [];
164
+
165
+ for await (const chunk of res) {
166
+ chunks.push(chunk);
167
+ }
168
+
169
+ return Buffer.concat(chunks).toString('utf8');
170
+ }
171
+
172
+ async function withBaseUrlCandidates(baseUrl, relativePath, operation) {
173
+ const candidates = buildBaseUrlCandidates(baseUrl);
174
+ let lastError;
175
+
176
+ for (const candidate of candidates) {
177
+ const url = `${candidate}${relativePath}`;
178
+
179
+ try {
180
+ const result = await operation(url);
181
+ return { result, baseUrl: candidate, url };
182
+ } catch (error) {
183
+ lastError = error;
184
+ }
185
+ }
186
+
187
+ throw lastError;
188
+ }
189
+
190
+ async function fetchJson(url, options = {}) {
191
+ const text = await fetchText(url, options);
192
+
193
+ try {
194
+ return JSON.parse(text);
195
+ } catch (error) {
196
+ throw new Error(`接口返回了无效 JSON: ${url}\n${error.message}`);
197
+ }
198
+ }
199
+
200
+ async function downloadToFile(url, filePath, options = {}) {
201
+ const res = await request(url, options);
202
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
203
+ await pipeline(res, createWriteStream(filePath));
204
+ }
205
+
206
+ async function md5File(filePath) {
207
+ const buffer = await fs.readFile(filePath);
208
+ return crypto.createHash('md5').update(buffer).digest('hex');
209
+ }
210
+
211
+ async function extractZip(zipPath, targetDir) {
212
+ await fs.mkdir(targetDir, { recursive: true });
213
+ await pipeline(createReadStream(zipPath), unzipper.Extract({ path: targetDir }));
214
+ }
215
+
216
+ async function findPackageRoot(extractDir) {
217
+ const rootMetadata = path.join(extractDir, 'aisoul.json');
218
+ if (await fileExists(rootMetadata)) {
219
+ return extractDir;
220
+ }
221
+
222
+ const entries = await fs.readdir(extractDir, { withFileTypes: true });
223
+ for (const entry of entries) {
224
+ if (!entry.isDirectory()) {
225
+ continue;
226
+ }
227
+
228
+ const candidate = path.join(extractDir, entry.name);
229
+ if (await fileExists(path.join(candidate, 'aisoul.json'))) {
230
+ return candidate;
231
+ }
232
+ }
233
+
234
+ throw new Error('解压后的安装包中未找到 aisoul.json');
235
+ }
236
+
237
+ async function copyPackageContent(sourceDir, targetDir) {
238
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
239
+
240
+ for (const entry of entries) {
241
+ if (entry.name === 'AGENTS.md') {
242
+ continue;
243
+ }
244
+
245
+ const sourcePath = path.join(sourceDir, entry.name);
246
+ const targetPath = path.join(targetDir, entry.name);
13
247
 
248
+ if (entry.isDirectory()) {
249
+ await fs.mkdir(targetPath, { recursive: true });
250
+ await copyPackageContent(sourcePath, targetPath);
251
+ continue;
252
+ }
253
+
254
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
255
+ await fs.writeFile(targetPath, await fs.readFile(sourcePath));
256
+ }
257
+ }
258
+
259
+ async function appendAgentsFile(sourceDir, workspacePath) {
260
+ const sourceAgentsPath = path.join(sourceDir, 'AGENTS.md');
261
+ if (!(await fileExists(sourceAgentsPath))) {
262
+ return false;
263
+ }
264
+
265
+ const workspaceAgentsPath = path.join(workspacePath, 'AGENTS.md');
266
+ const [sourceContent, existingContent] = await Promise.all([
267
+ fs.readFile(sourceAgentsPath, 'utf8'),
268
+ fileExists(workspaceAgentsPath)
269
+ ? fs.readFile(workspaceAgentsPath, 'utf8')
270
+ : Promise.resolve(''),
271
+ ]);
272
+
273
+ await fs.writeFile(
274
+ workspaceAgentsPath,
275
+ buildMergedAgentsContent(existingContent, sourceContent),
276
+ 'utf8'
277
+ );
278
+
279
+ return true;
280
+ }
281
+
282
+ async function runCommand(command, args) {
283
+ return new Promise((resolve, reject) => {
284
+ const child = spawn(command, args, {
285
+ stdio: ['ignore', 'pipe', 'pipe'],
286
+ env: process.env,
287
+ });
288
+
289
+ let stdout = '';
290
+ let stderr = '';
291
+
292
+ child.stdout.on('data', chunk => {
293
+ stdout += chunk.toString();
294
+ });
295
+
296
+ child.stderr.on('data', chunk => {
297
+ stderr += chunk.toString();
298
+ });
299
+
300
+ child.on('error', reject);
301
+ child.on('close', code => {
302
+ if (code === 0) {
303
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
304
+ return;
305
+ }
306
+
307
+ reject(
308
+ new Error(
309
+ [
310
+ `命令执行失败: ${command} ${args.join(' ')}`,
311
+ stdout.trim(),
312
+ stderr.trim(),
313
+ ]
314
+ .filter(Boolean)
315
+ .join('\n')
316
+ )
317
+ );
318
+ });
319
+ });
320
+ }
321
+
322
+ export function summarizeErrorMessage(error) {
323
+ const lines = String(error?.message || error || '')
324
+ .split('\n')
325
+ .map(line => line.trim())
326
+ .filter(Boolean);
327
+
328
+ if (lines.length === 0) {
329
+ return '未知错误';
330
+ }
331
+
332
+ for (const line of lines) {
333
+ const match = line.match(/"Message"\s*:\s*"([^"]+)"/);
334
+ if (match?.[1]) {
335
+ return match[1];
336
+ }
337
+ }
338
+
339
+ const detailLine = lines.find(line => !line.startsWith('命令执行失败:'));
340
+ return detailLine || lines[0];
341
+ }
342
+
343
+ async function listAgents() {
344
+ const { stdout } = await runCommand('openclaw', ['agents', 'list', '--json']);
345
+
346
+ if (!stdout) {
347
+ return [];
348
+ }
349
+
350
+ try {
351
+ return JSON.parse(stdout);
352
+ } catch (error) {
353
+ throw new Error(`无法解析 openclaw agents list --json 输出\n${error.message}`);
354
+ }
355
+ }
356
+
357
+ export function buildExistingAgentDeleteMessage(agentName, workspacePath) {
358
+ return [
359
+ `检测到已存在同名 agent "${agentName}",请先删除后再安装。`,
360
+ `workspace: ${workspacePath}`,
361
+ `请执行: openclaw agents delete ${agentName}`,
362
+ ].join('\n');
363
+ }
364
+
365
+ async function resolveVersion(baseUrl, slug, user) {
366
+ const headers = buildRequestHeaders(user);
367
+ const { result: payload, baseUrl: resolvedBaseUrl } = await withBaseUrlCandidates(
368
+ baseUrl,
369
+ `/soul/info?slug=${encodeURIComponent(slug)}`,
370
+ url => fetchJson(url, { headers })
371
+ );
372
+ const version = payload?.data?.version;
373
+
374
+ if (!version) {
375
+ throw new Error(`接口未返回 ${slug} 的版本信息`);
376
+ }
377
+
378
+ return { version, baseUrl: resolvedBaseUrl };
379
+ }
380
+
381
+ async function installSkills(skills, workspacePath) {
382
+ if (!Array.isArray(skills) || skills.length === 0) {
383
+ return {
384
+ installedSkills: [],
385
+ skippedSkills: [],
386
+ };
387
+ }
388
+
389
+ const installedSkills = [];
390
+ const skippedSkills = [];
391
+
392
+ for (const skill of skills) {
393
+ const skillName = normalizeSkillName(skill);
394
+ if (!skillName) {
395
+ continue;
396
+ }
397
+
398
+ try {
399
+ await runCommand('npx', [
400
+ 'clawhub',
401
+ 'install',
402
+ skillName,
403
+ '--workdir',
404
+ workspacePath,
405
+ '--registry',
406
+ CLAWHUB_REGISTRY,
407
+ ]);
408
+
409
+ installedSkills.push({
410
+ requested: String(skill),
411
+ installed: skillName,
412
+ });
413
+ } catch (error) {
414
+ const reason = summarizeErrorMessage(error);
415
+ skippedSkills.push({
416
+ requested: String(skill),
417
+ installed: skillName,
418
+ reason,
419
+ });
420
+ console.warn(`- 跳过 skill: ${skillName}${reason ? ` (${reason})` : ''}`);
421
+ }
422
+ }
423
+
424
+ return {
425
+ installedSkills,
426
+ skippedSkills,
427
+ };
428
+ }
429
+
430
+ function printSummary(summary) {
431
+ console.log('\n安装完成');
432
+ console.log(`- slug: ${summary.slug}`);
433
+ console.log(`- version: ${summary.version}`);
434
+ console.log(`- user: ${summary.user}`);
435
+ console.log(`- workspace: ${summary.workspacePath}`);
436
+ console.log(`- agent: ${summary.agentName}`);
437
+ console.log(`- display_name: ${summary.displayName}`);
438
+ console.log(`- base_url: ${summary.baseUrl}`);
439
+ console.log(`- agents_appended: ${summary.agentsAppended ? 'yes' : 'no'}`);
440
+ console.log(
441
+ `- skills: ${
442
+ summary.installedSkills.length > 0
443
+ ? summary.installedSkills.map(item => item.installed).join(', ')
444
+ : 'none'
445
+ }`
446
+ );
447
+ console.log(
448
+ `- skipped_skills: ${
449
+ summary.skippedSkills.length > 0
450
+ ? summary.skippedSkills
451
+ .map(item => `${item.installed}${item.reason ? ` (${item.reason})` : ''}`)
452
+ .join(', ')
453
+ : 'none'
454
+ }`
455
+ );
456
+ }
457
+
458
+ export async function installSoul({ soulSpecifier, user, baseUrl }) {
459
+ const { slug, version: rawVersion } = parseSoulSpecifier(soulSpecifier);
460
+ let effectiveBaseUrl = normalizeBaseUrl(baseUrl);
461
+ let version = rawVersion;
462
+ const defaultWorkspacePath = path.join(os.homedir(), '.openclaw', `workspace-${slug}`);
463
+ const agentName = slug;
464
+ const existingAgent = findExistingAgent(await listAgents(), agentName);
465
+
466
+ if (existingAgent) {
467
+ throw new UserFacingError(
468
+ buildExistingAgentDeleteMessage(
469
+ agentName,
470
+ existingAgent.workspace || defaultWorkspacePath
471
+ )
472
+ );
473
+ }
474
+
475
+ if (!version) {
476
+ const resolved = await resolveVersion(effectiveBaseUrl, slug, user);
477
+ version = resolved.version;
478
+ effectiveBaseUrl = resolved.baseUrl;
479
+ }
480
+
481
+ const packageName = `${slug}-${version}.zip`;
482
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aisoulhub-'));
483
+ const zipPath = path.join(tempRoot, packageName);
484
+ const extractDir = path.join(tempRoot, 'unzipped');
485
+ const workspacePath = defaultWorkspacePath;
486
+
487
+ try {
488
+ const requestHeaders = buildRequestHeaders(user);
489
+ const zipRelativePath = `/package/${packageName}`;
490
+ const md5RelativePath = `${zipRelativePath}.md5`;
491
+
492
+ console.log(`开始安装 ${slug}${rawVersion ? `@${version}` : `,解析到版本 ${version}`}`);
493
+
494
+ const { baseUrl: resolvedPackageBaseUrl, url: zipUrl } = await withBaseUrlCandidates(
495
+ effectiveBaseUrl,
496
+ zipRelativePath,
497
+ url => downloadToFile(url, zipPath, { headers: requestHeaders })
498
+ );
499
+ effectiveBaseUrl = resolvedPackageBaseUrl;
500
+ const md5Url = `${effectiveBaseUrl}${md5RelativePath}`;
501
+
502
+ console.log(`下载地址: ${zipUrl}`);
503
+
504
+ const remoteMd5 = extractMd5Hash(
505
+ await fetchText(md5Url, { headers: requestHeaders })
506
+ );
507
+ const localMd5 = await md5File(zipPath);
508
+
509
+ if (remoteMd5 !== localMd5) {
510
+ throw new Error(`MD5 校验失败,期望 ${remoteMd5},实际 ${localMd5}`);
511
+ }
512
+
513
+ await extractZip(zipPath, extractDir);
514
+ const packageRoot = await findPackageRoot(extractDir);
515
+ const aisoulJsonPath = path.join(packageRoot, 'aisoul.json');
516
+ const metadata = JSON.parse(await fs.readFile(aisoulJsonPath, 'utf8'));
517
+ const displayName = String(metadata.nick_name || slug).trim() || slug;
518
+
519
+ await runCommand('openclaw', [
520
+ 'agents',
521
+ 'add',
522
+ agentName,
523
+ '--non-interactive',
524
+ '--workspace',
525
+ workspacePath,
526
+ ]);
527
+
528
+ await runCommand('openclaw', [
529
+ 'agents',
530
+ 'set-identity',
531
+ '--agent',
532
+ agentName,
533
+ '--name',
534
+ displayName,
535
+ ]);
536
+
537
+ await fs.mkdir(workspacePath, { recursive: true });
538
+ await copyPackageContent(packageRoot, workspacePath);
539
+ const agentsAppended = await appendAgentsFile(packageRoot, workspacePath);
540
+ const { installedSkills, skippedSkills } = await installSkills(
541
+ metadata.skills,
542
+ workspacePath
543
+ );
544
+
545
+ await runCommand('openclaw', [
546
+ 'agent',
547
+ '--agent',
548
+ agentName,
549
+ '--message',
550
+ `Hi,你的名字是 ${displayName}`,
551
+ ]);
552
+
553
+ printSummary({
554
+ slug,
555
+ version,
556
+ user,
557
+ workspacePath,
558
+ agentName,
559
+ displayName,
560
+ baseUrl: effectiveBaseUrl,
561
+ agentsAppended,
562
+ installedSkills,
563
+ skippedSkills,
564
+ });
565
+ } finally {
566
+ await fs.rm(tempRoot, { recursive: true, force: true });
567
+ }
568
+ }
569
+
570
+ export async function main(argv = process.argv) {
14
571
  const program = new Command();
15
572
 
16
573
  program
17
574
  .name('aisoulhub')
18
- .description('AISoulHub CLI tool for installing Agent skills and configurations')
19
- .version(pkg.version);
575
+ .description('安装和更新 aisoul OpenClaw workspace')
576
+ .showHelpAfterError();
20
577
 
21
578
  program
22
579
  .command('install')
23
- .description('Install new Agent configuration and skills from a local zip file')
24
- .argument('<path_to_zip>', 'Specify the local path to the zip file')
25
- .action(async (aiSoulId) => {
26
- await installCommand(aiSoulId);
580
+ .description('安装指定 aisoul 到本地 OpenClaw workspace')
581
+ .argument('<soul>', 'aisoul 标识,格式为 slug slug@version')
582
+ .requiredOption('--user <email>', '用于标识当前安装用户')
583
+ .option('--base_url <url>', 'aisoulhub 服务地址', DEFAULT_BASE_URL)
584
+ .action(async (soul, options) => {
585
+ await installSoul({
586
+ soulSpecifier: soul,
587
+ user: options.user,
588
+ baseUrl: options.base_url,
589
+ });
590
+ });
591
+
592
+ program
593
+ .command('update')
594
+ .description('预留的更新命令')
595
+ .action(() => {
596
+ console.error('update 命令暂未实现');
597
+ process.exit(1);
27
598
  });
28
599
 
29
- program.parseAsync(process.argv);
600
+ await program.parseAsync(argv);
30
601
  }
@@ -1,164 +0,0 @@
1
- import { checkOpenclaw } from '../utils/check.js';
2
- import { getAgents, addAgent, setAgentIdentity } from '../utils/openclaw.js';
3
- import { extractLocalZip } from '../utils/download.js';
4
- import { processMetaJson } from '../utils/meta.js';
5
- import { select, input, confirm } from '@inquirer/prompts';
6
- import chalk from 'chalk';
7
- import fs from 'fs';
8
- import path from 'path';
9
- import os from 'os';
10
- import unzipper from 'unzipper';
11
-
12
- export async function installCommand(zipPath) {
13
- if (!zipPath) {
14
- console.error(chalk.red('❌ Error: zip path is required, e.g.: aisoulhub install <path_to_zip>'));
15
- process.exit(1);
16
- }
17
-
18
- // 检查是否为本地 zip 文件
19
- if (!zipPath.endsWith('.zip') || !fs.existsSync(path.resolve(zipPath))) {
20
- console.error(chalk.red(`❌ Error: Invalid or missing local zip file at [${zipPath}]`));
21
- process.exit(1);
22
- }
23
- const localZipPath = path.resolve(zipPath);
24
-
25
- // 1. 前置检查
26
- checkOpenclaw();
27
-
28
- // 2. 获取当前的 agents 列表
29
- let agents = [];
30
- try {
31
- agents = getAgents();
32
- } catch (err) {
33
- console.error(chalk.yellow(`⚠️ Failed to get agents list: ${err.message}`));
34
- }
35
-
36
- // 3. 提示用户选择或创建 Agent
37
- const choices = [
38
- { name: '✨ Create new Agent', value: { name: 'create_new', path: null } },
39
- ...agents.map(a => ({ name: `📁 ${a.name} (${a.path})`, value: { name: a.name, path: a.path } }))
40
- ];
41
-
42
- const agentChoice = await select({
43
- message: 'Please select the Agent to install:',
44
- choices
45
- });
46
-
47
- if (agentChoice.name === 'main') {
48
- const isConfirmed = await confirm({
49
- message: chalk.yellow('⚠️ Warning: Overwriting the "main" agent might cause unexpected issues. Are you sure you want to continue?'),
50
- default: false
51
- });
52
-
53
- if (!isConfirmed) {
54
- console.log(chalk.blue('ℹ️ Operation cancelled by user.'));
55
- process.exit(0);
56
- }
57
- }
58
-
59
- let targetPath = agentChoice.path;
60
- let finalAgentName = agentChoice.name;
61
-
62
- if (agentChoice.name === 'create_new') {
63
- let defaultAgentName = '';
64
- let identityName = '';
65
-
66
- // 尝试从 zip 文件中的 aisoul-meta.json 获取默认 name 和 slug
67
- try {
68
- const directory = await unzipper.Open.file(localZipPath);
69
- const metaFile = directory.files.find(d => d.path === 'aisoul-meta.json');
70
- if (metaFile) {
71
- const content = await metaFile.buffer();
72
- const meta = JSON.parse(content.toString('utf-8'));
73
- if (meta.slug) {
74
- defaultAgentName = meta.slug;
75
- }
76
- if (meta.name) {
77
- identityName = meta.name;
78
- }
79
- }
80
- } catch (err) {
81
- // 忽略读取 zip 出错的情况
82
- }
83
-
84
- const newAgentName = await input({
85
- message: 'Please enter the name of the new Agent:',
86
- default: defaultAgentName,
87
- validate: (val) => {
88
- if (val.trim().length === 0) return 'Agent name cannot be empty';
89
- if (val.trim().toLowerCase() === 'main') return '"main" is reserved. Choose another name.';
90
- return true;
91
- }
92
- });
93
-
94
- finalAgentName = newAgentName;
95
-
96
- const defaultWorkspace = path.join(os.homedir(), '.openclaw', `workspace-${newAgentName}`);
97
-
98
- const newWorkspacePath = await input({
99
- message: 'Please enter the workspace directory for the new Agent:',
100
- default: defaultWorkspace,
101
- validate: (val) => val.trim().length > 0 ? true : 'Workspace directory cannot be empty'
102
- });
103
-
104
- console.log(chalk.cyan(`🔄 Creating Agent [${newAgentName}]...`));
105
- try {
106
- addAgent(newAgentName, newWorkspacePath);
107
- console.log(chalk.green(`✅ Agent [${newAgentName}] created successfully.`));
108
-
109
- // 设置 Agent 的显示名称
110
- if (identityName) {
111
- try {
112
- setAgentIdentity(newAgentName, identityName);
113
- console.log(chalk.green(`✅ Agent identity name set to [${identityName}].`));
114
- } catch (identityErr) {
115
- console.error(chalk.yellow(`⚠️ Warning: Failed to set identity name: ${identityErr.message}`));
116
- }
117
- }
118
-
119
- // 重新获取列表以获取新 agent 的路径
120
- const updatedAgents = getAgents();
121
- const newAgent = updatedAgents.find(a => a.name === newAgentName);
122
- if (!newAgent || !newAgent.path) {
123
- targetPath = newWorkspacePath;
124
- } else {
125
- targetPath = newAgent.path;
126
- }
127
- } catch (err) {
128
- console.error(chalk.red(err.message));
129
- process.exit(1);
130
- }
131
- }
132
-
133
- if (!targetPath) {
134
- console.error(chalk.red('❌ Error: Unable to determine the workspace directory path for the Agent.'));
135
- process.exit(1);
136
- }
137
-
138
- console.log(chalk.blue(`ℹ️ Target workspace directory: ${targetPath}`));
139
-
140
- // 4. 解压本地 ZIP 文件
141
- const extractSuccess = await extractLocalZip(localZipPath, targetPath);
142
-
143
- if (!extractSuccess) {
144
- process.exit(1);
145
- }
146
-
147
- // 5. 解析 aisoul-meta.json 并安装技能
148
- await processMetaJson(targetPath);
149
-
150
- console.log(chalk.green(`🎉 Successfully installed local package to the Agent.`));
151
-
152
- // 直接唤醒 Agent
153
- console.log(chalk.cyan(`\n👉 Waking up the agent...`));
154
- const { spawn } = await import('child_process');
155
-
156
- const child = spawn('openclaw', ['agent', '--agent', finalAgentName, '--message', 'hi'], {
157
- stdio: 'inherit',
158
- shell: true
159
- });
160
-
161
- child.on('error', (error) => {
162
- console.error(chalk.red(`❌ Failed to start the agent: ${error.message}`));
163
- });
164
- }
@@ -1,9 +0,0 @@
1
- import shell from 'shelljs';
2
- import chalk from 'chalk';
3
-
4
- export function checkOpenclaw() {
5
- if (!shell.which('openclaw')) {
6
- console.error(chalk.red('❌ Error: openclaw command not found. Please follow the instructions at https://openclaw.ai to install openclaw, then try again.'));
7
- process.exit(1);
8
- }
9
- }
@@ -1,26 +0,0 @@
1
- import fs from 'fs';
2
- import unzipper from 'unzipper';
3
- import ora from 'ora';
4
- import chalk from 'chalk';
5
-
6
- export async function extractLocalZip(zipPath, targetPath) {
7
- const spinner = ora(`Extracting local package from ${zipPath}...`).start();
8
-
9
- try {
10
- // Ensure target path exists
11
- if (!fs.existsSync(targetPath)) {
12
- fs.mkdirSync(targetPath, { recursive: true });
13
- }
14
-
15
- // Unzip
16
- await fs.createReadStream(zipPath)
17
- .pipe(unzipper.Extract({ path: targetPath }))
18
- .promise();
19
-
20
- spinner.succeed(chalk.green(`Local package extracted successfully.`));
21
- return true;
22
- } catch (err) {
23
- spinner.fail(chalk.red(`Error: Failed to extract the local package: ${err.message}`));
24
- return false;
25
- }
26
- }
package/src/utils/meta.js DELETED
@@ -1,25 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import chalk from 'chalk';
4
- import { installSkill } from './openclaw.js';
5
-
6
- export async function processMetaJson(workspacePath) {
7
- const metaPath = path.join(workspacePath, 'aisoul-meta.json');
8
- if (!fs.existsSync(metaPath)) {
9
- console.log(chalk.yellow(`ℹ️ aisoul-meta.json not found, skipping skill installation. (${metaPath})`));
10
- return;
11
- }
12
-
13
- try {
14
- const content = fs.readFileSync(metaPath, 'utf-8');
15
- const meta = JSON.parse(content);
16
- const skills = meta.skills || [];
17
-
18
- if (skills.length === 0) {
19
- console.log(chalk.blue('ℹ️ No skills defined to install in aisoul-meta.json.'));
20
- return;
21
- }
22
- } catch (err) {
23
- console.error(chalk.red(`❌ Failed to read or parse aisoul-meta.json: ${err.message}`));
24
- }
25
- }
@@ -1,97 +0,0 @@
1
- import shell from 'shelljs';
2
-
3
- export function getAgents() {
4
- const result = shell.exec('openclaw agents list --json', { silent: true });
5
- if (result.code !== 0) {
6
- throw new Error('Failed to get agents list: ' + result.stderr);
7
- }
8
-
9
- // Try to parse JSON first
10
- try {
11
- // 尝试提取标准 JSON 数组结构(截取第一个 '[' 到匹配的 ']' 的内容)
12
- // 为了防止截取到后面的无关内容,我们查找从 '[' 开始,以 ']' 结束,并且紧跟换行或结尾的部分
13
- const jsonMatch = result.stdout.match(/\[[\s\S]*?\n\]/);
14
- if (jsonMatch) {
15
- const list = JSON.parse(jsonMatch[0]);
16
- if (Array.isArray(list)) {
17
- return list.map(item => ({
18
- name: item.id || item.name,
19
- path: item.workspace || item.path || item.agentDir
20
- }));
21
- }
22
- } else {
23
- // 如果没有匹配到数组结构,则尝试直接解析
24
- const list = JSON.parse(result.stdout);
25
- if (Array.isArray(list)) {
26
- return list.map(item => ({
27
- name: item.id || item.name,
28
- path: item.workspace || item.path || item.agentDir
29
- }));
30
- }
31
- }
32
- } catch (err) {
33
- // Not JSON, parse text
34
- }
35
-
36
- // Fallback text parsing
37
- const lines = result.stdout.split('\n');
38
- const agents = [];
39
-
40
- // Regex to match agent name and path. E.g., "- agent1 (/path/to/agent1)" or "agent1: /path/to/agent1"
41
- // Let's just do a generic parsing or return raw lines if we can't be sure.
42
- for (const line of lines) {
43
- const match = line.match(/(?:-\s+)?(\w+)(?:\s+\((.*?)\)|:\s+(.*))?/);
44
- if (match && match[1] && match[1] !== 'Agents') {
45
- const name = match[1].trim();
46
- const pathStr = (match[2] || match[3] || '').trim();
47
- if (name) {
48
- agents.push({ name, path: pathStr || null });
49
- }
50
- }
51
- }
52
- return agents;
53
- }
54
-
55
- export function addAgent(name, workspacePath) {
56
- let cmd = `openclaw agents add "${name}"`;
57
- if (workspacePath) {
58
- cmd += ` --non-interactive --workspace "${workspacePath}"`;
59
- }
60
- const result = shell.exec(cmd, { silent: true });
61
- if (result.code !== 0) {
62
- throw new Error(`Failed to create agent [${name}]: ` + result.stderr);
63
- }
64
- return result.stdout;
65
- }
66
-
67
- export function setAgentIdentity(agentName, identityName) {
68
- const cmd = `openclaw agents set-identity --agent "${agentName}" --name "${identityName}"`;
69
- const result = shell.exec(cmd, { silent: true });
70
- if (result.code !== 0) {
71
- throw new Error(`Failed to set agent identity for [${agentName}]: ` + result.stderr);
72
- }
73
- return result.stdout;
74
- }
75
-
76
- export async function installSkill(workspacePath, skillStr, retryCount = 0) {
77
- const maxRetries = 3;
78
- const result = shell.exec(`npx clawhub install ${skillStr} --workdir "${workspacePath}"`, { silent: true });
79
-
80
- if (result.code !== 0) {
81
- const errorOutput = result.stderr || result.stdout;
82
- if (errorOutput.includes('Rate limit exceeded')) {
83
- if (retryCount < maxRetries) {
84
- console.log(`⚠️ Rate limit exceeded. Waiting 5 seconds before retrying (${retryCount + 1}/${maxRetries})...`);
85
- await new Promise(resolve => setTimeout(resolve, 5000));
86
- return installSkill(workspacePath, skillStr, retryCount + 1);
87
- } else {
88
- console.error(`❌ Failed to install skill ${skillStr} after ${maxRetries} retries due to rate limit.`);
89
- return false;
90
- }
91
- }
92
-
93
- console.error(`❌ Failed to install skill ${skillStr}: `, errorOutput);
94
- return false;
95
- }
96
- return true;
97
- }