aisoulhub 1.0.8 → 1.1.2

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,54 +1,58 @@
1
1
  # AISoulHub CLI
2
2
 
3
- A powerful 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:
8
- - [Node.js](https://nodejs.org/) (v16 or higher recommended)
9
- - [OpenClaw](https://openclaw.ai/) CLI (`openclaw` command must be available in your PATH)
7
+ - [Node.js](https://nodejs.org/) v16 或更高版本
8
+ - 已安装并可直接执行的 `openclaw`
9
+ - 可通过 `npx` 执行 `clawhub`
10
10
 
11
- ## Usage
11
+ ## 使用方式
12
12
 
13
- You can run `aisoulhub` directly via `npx` without needing to install it globally.
13
+ ### 安装 aisoul
14
14
 
15
- ### 1. Install an AI Soul
15
+ ```bash
16
+ npx aisoulhub install <slug[@version]> --user <your@email.com>
17
+ ```
16
18
 
17
- Downloads and installs an AI Soul package into a new or existing OpenClaw Agent.
19
+ 示例:
18
20
 
19
21
  ```bash
20
- npx aisoulhub install <ai_soul_id>
22
+ npx aisoulhub install helloworld --user your@gmail.com
21
23
  ```
22
24
 
23
- **What happens during install?**
24
- - Checks for OpenClaw dependency.
25
- - Prompts for your email (if not already logged in) and saves it to `~/.aisoulhub.json`.
26
- - Asks you to select an existing Agent or create a new one.
27
- - Downloads the corresponding zip package from AISoulHub.
28
- - Extracts files (like `SOUL.md`, `AGENTS.md`, `IDENTITY.md`, `aisoul-meta.json`) into the Agent's workspace.
29
- - Automatically reads `aisoul-meta.json` and installs all defined skills via OpenClaw.
25
+ 指定版本:
30
26
 
31
- ### 2. Update an AI Soul
27
+ ```bash
28
+ npx aisoulhub install helloworld@1.0.2 --user your@gmail.com
29
+ ```
32
30
 
33
- Updates an existing OpenClaw Agent with the latest AI Soul package configuration and skills.
31
+ 使用自定义服务地址:
34
32
 
35
33
  ```bash
36
- npx aisoulhub update
34
+ npx aisoulhub install helloworld --user your@gmail.com --base_url=https://aisoulhub.com
37
35
  ```
38
36
 
39
- **What happens during update?**
40
- - Displays a list of your existing Agents for you to select.
41
- - Attempts to read the `ai_soul_id` from the Agent's `aisoul-meta.json`. If missing, prompts you to input it.
42
- - Downloads the latest zip package from AISoulHub.
43
- - Extracts and overwrites the existing configuration files in the workspace.
44
- - Updates/re-installs the skills defined in the new `aisoul-meta.json`.
37
+ 参数说明:
45
38
 
46
- ## Configuration
39
+ - `<slug[@version]>`:aisoul 标识,`@version` 可选
40
+ - `--user`:请使用你的注册邮箱
41
+ - `--base_url`:可选,自定义服务地址
47
42
 
48
- The CLI saves a minimal configuration file at `~/.aisoulhub.json` which currently stores your login email to streamline subsequent command executions.
43
+ 安装提示:
49
44
 
50
- ## License
45
+ - 如果未指定版本,将自动安装该 aisoul 的最新版本
46
+ - 如果已存在同名 agent,请先执行 `openclaw agents delete <agentName>` 后再重新安装
51
47
 
52
- ISC
48
+ ### 更新 aisoul
53
49
 
50
+ ```bash
51
+ npx aisoulhub update
52
+ ```
53
+
54
+ `update` 命令已预留,当前版本暂未实现。
54
55
 
56
+ ## License
57
+
58
+ ISC
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.8",
3
+ "version": "1.1.2",
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,8 +22,6 @@
22
22
  },
23
23
  "type": "module",
24
24
  "dependencies": {
25
- "@inquirer/prompts": "^8.3.2",
26
- "axios": "^1.13.6",
27
25
  "chalk": "^5.6.2",
28
26
  "clawhub": "^0.9.0",
29
27
  "commander": "^14.0.3",
package/src/index.js CHANGED
@@ -1,38 +1,550 @@
1
1
  import { Command } from 'commander';
2
- import { installCommand } from './commands/install.js';
3
- import { updateCommand } from './commands/update.js';
4
- import fs from 'fs';
5
- import path from 'path';
6
- 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';
7
12
 
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
13
+ export const DEFAULT_BASE_URL = 'https://aisouhub.com';
14
+ const CLAWHUB_REGISTRY = 'https://cn.clawhub-mirror.com';
10
15
 
11
- export async function main() {
12
- const pkgPath = path.join(__dirname, '../package.json');
13
- 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);
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
+ async function listAgents() {
323
+ const { stdout } = await runCommand('openclaw', ['agents', 'list', '--json']);
324
+
325
+ if (!stdout) {
326
+ return [];
327
+ }
328
+
329
+ try {
330
+ return JSON.parse(stdout);
331
+ } catch (error) {
332
+ throw new Error(`无法解析 openclaw agents list --json 输出\n${error.message}`);
333
+ }
334
+ }
335
+
336
+ export function buildExistingAgentDeleteMessage(agentName, workspacePath) {
337
+ return [
338
+ `检测到已存在同名 agent "${agentName}",请先删除后再安装。`,
339
+ `workspace: ${workspacePath}`,
340
+ `请执行: openclaw agents delete ${agentName}`,
341
+ ].join('\n');
342
+ }
343
+
344
+ async function resolveVersion(baseUrl, slug, user) {
345
+ const headers = buildRequestHeaders(user);
346
+ const { result: payload, baseUrl: resolvedBaseUrl } = await withBaseUrlCandidates(
347
+ baseUrl,
348
+ `/soul/info?slug=${encodeURIComponent(slug)}`,
349
+ url => fetchJson(url, { headers })
350
+ );
351
+ const version = payload?.data?.version;
352
+
353
+ if (!version) {
354
+ throw new Error(`接口未返回 ${slug} 的版本信息`);
355
+ }
356
+
357
+ return { version, baseUrl: resolvedBaseUrl };
358
+ }
359
+
360
+ async function installSkills(skills, workspacePath) {
361
+ if (!Array.isArray(skills) || skills.length === 0) {
362
+ return [];
363
+ }
364
+
365
+ const installedSkills = [];
366
+
367
+ for (const skill of skills) {
368
+ const skillName = normalizeSkillName(skill);
369
+ if (!skillName) {
370
+ continue;
371
+ }
372
+
373
+ await runCommand('npx', [
374
+ 'clawhub',
375
+ 'install',
376
+ skillName,
377
+ '--workdir',
378
+ workspacePath,
379
+ '--registry',
380
+ CLAWHUB_REGISTRY,
381
+ ]);
382
+
383
+ installedSkills.push({
384
+ requested: String(skill),
385
+ installed: skillName,
386
+ });
387
+ }
388
+
389
+ return installedSkills;
390
+ }
391
+
392
+ function printSummary(summary) {
393
+ console.log('\n安装完成');
394
+ console.log(`- slug: ${summary.slug}`);
395
+ console.log(`- version: ${summary.version}`);
396
+ console.log(`- user: ${summary.user}`);
397
+ console.log(`- workspace: ${summary.workspacePath}`);
398
+ console.log(`- agent: ${summary.agentName}`);
399
+ console.log(`- display_name: ${summary.displayName}`);
400
+ console.log(`- base_url: ${summary.baseUrl}`);
401
+ console.log(`- agents_appended: ${summary.agentsAppended ? 'yes' : 'no'}`);
402
+ console.log(
403
+ `- skills: ${
404
+ summary.installedSkills.length > 0
405
+ ? summary.installedSkills.map(item => item.installed).join(', ')
406
+ : 'none'
407
+ }`
408
+ );
409
+ }
410
+
411
+ export async function installSoul({ soulSpecifier, user, baseUrl }) {
412
+ const { slug, version: rawVersion } = parseSoulSpecifier(soulSpecifier);
413
+ let effectiveBaseUrl = normalizeBaseUrl(baseUrl);
414
+ let version = rawVersion;
415
+ const defaultWorkspacePath = path.join(os.homedir(), '.openclaw', `workspace-${slug}`);
416
+ const agentName = slug;
417
+ const existingAgent = findExistingAgent(await listAgents(), agentName);
418
+
419
+ if (existingAgent) {
420
+ throw new UserFacingError(
421
+ buildExistingAgentDeleteMessage(
422
+ agentName,
423
+ existingAgent.workspace || defaultWorkspacePath
424
+ )
425
+ );
426
+ }
427
+
428
+ if (!version) {
429
+ const resolved = await resolveVersion(effectiveBaseUrl, slug, user);
430
+ version = resolved.version;
431
+ effectiveBaseUrl = resolved.baseUrl;
432
+ }
433
+
434
+ const packageName = `${slug}-${version}.zip`;
435
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aisoulhub-'));
436
+ const zipPath = path.join(tempRoot, packageName);
437
+ const extractDir = path.join(tempRoot, 'unzipped');
438
+ const workspacePath = defaultWorkspacePath;
439
+
440
+ try {
441
+ const requestHeaders = buildRequestHeaders(user);
442
+ const zipRelativePath = `/package/${packageName}`;
443
+ const md5RelativePath = `${zipRelativePath}.md5`;
444
+
445
+ console.log(`开始安装 ${slug}${rawVersion ? `@${version}` : `,解析到版本 ${version}`}`);
446
+
447
+ const { baseUrl: resolvedPackageBaseUrl, url: zipUrl } = await withBaseUrlCandidates(
448
+ effectiveBaseUrl,
449
+ zipRelativePath,
450
+ url => downloadToFile(url, zipPath, { headers: requestHeaders })
451
+ );
452
+ effectiveBaseUrl = resolvedPackageBaseUrl;
453
+ const md5Url = `${effectiveBaseUrl}${md5RelativePath}`;
454
+
455
+ console.log(`下载地址: ${zipUrl}`);
456
+
457
+ const remoteMd5 = extractMd5Hash(
458
+ await fetchText(md5Url, { headers: requestHeaders })
459
+ );
460
+ const localMd5 = await md5File(zipPath);
461
+
462
+ if (remoteMd5 !== localMd5) {
463
+ throw new Error(`MD5 校验失败,期望 ${remoteMd5},实际 ${localMd5}`);
464
+ }
465
+
466
+ await extractZip(zipPath, extractDir);
467
+ const packageRoot = await findPackageRoot(extractDir);
468
+ const aisoulJsonPath = path.join(packageRoot, 'aisoul.json');
469
+ const metadata = JSON.parse(await fs.readFile(aisoulJsonPath, 'utf8'));
470
+ const displayName = String(metadata.nick_name || slug).trim() || slug;
471
+
472
+ await runCommand('openclaw', [
473
+ 'agents',
474
+ 'add',
475
+ agentName,
476
+ '--non-interactive',
477
+ '--workspace',
478
+ workspacePath,
479
+ ]);
480
+
481
+ await runCommand('openclaw', [
482
+ 'agents',
483
+ 'set-identity',
484
+ '--agent',
485
+ agentName,
486
+ '--name',
487
+ displayName,
488
+ ]);
489
+
490
+ await fs.mkdir(workspacePath, { recursive: true });
491
+ await copyPackageContent(packageRoot, workspacePath);
492
+ const agentsAppended = await appendAgentsFile(packageRoot, workspacePath);
493
+ const installedSkills = await installSkills(metadata.skills, workspacePath);
494
+
495
+ await runCommand('openclaw', [
496
+ 'agent',
497
+ '--agent',
498
+ agentName,
499
+ '--message',
500
+ 'Hi',
501
+ ]);
502
+
503
+ printSummary({
504
+ slug,
505
+ version,
506
+ user,
507
+ workspacePath,
508
+ agentName,
509
+ displayName,
510
+ baseUrl: effectiveBaseUrl,
511
+ agentsAppended,
512
+ installedSkills,
513
+ });
514
+ } finally {
515
+ await fs.rm(tempRoot, { recursive: true, force: true });
516
+ }
517
+ }
14
518
 
519
+ export async function main(argv = process.argv) {
15
520
  const program = new Command();
16
521
 
17
522
  program
18
523
  .name('aisoulhub')
19
- .description('AISoulHub CLI tool for installing and updating Agent skills and configurations')
20
- .version(pkg.version);
524
+ .description('安装和更新 aisoul OpenClaw workspace')
525
+ .showHelpAfterError();
21
526
 
22
527
  program
23
528
  .command('install')
24
- .description('Install new Agent configuration and skills')
25
- .argument('<ai_soul_id>', 'Specify the ai_soul_id to install (e.g.: 12345)')
26
- .action(async (aiSoulId) => {
27
- await installCommand(aiSoulId);
529
+ .description('安装指定 aisoul 到本地 OpenClaw workspace')
530
+ .argument('<soul>', 'aisoul 标识,格式为 slug slug@version')
531
+ .requiredOption('--user <email>', '用于标识当前安装用户')
532
+ .option('--base_url <url>', 'aisoulhub 服务地址', DEFAULT_BASE_URL)
533
+ .action(async (soul, options) => {
534
+ await installSoul({
535
+ soulSpecifier: soul,
536
+ user: options.user,
537
+ baseUrl: options.base_url,
538
+ });
28
539
  });
29
540
 
30
541
  program
31
542
  .command('update')
32
- .description('Update existing Agent configuration and skills')
33
- .action(async () => {
34
- await updateCommand();
543
+ .description('预留的更新命令')
544
+ .action(() => {
545
+ console.error('update 命令暂未实现');
546
+ process.exit(1);
35
547
  });
36
548
 
37
- program.parseAsync(process.argv);
549
+ await program.parseAsync(argv);
38
550
  }
@@ -1,103 +0,0 @@
1
- import { checkOpenclaw, checkLogin } from '../utils/check.js';
2
- import { getAgents, addAgent } from '../utils/openclaw.js';
3
- import { downloadAndExtract } 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 path from 'path';
8
- import os from 'os';
9
-
10
- export async function installCommand(aiSoulId) {
11
- if (!aiSoulId) {
12
- console.error(chalk.red('❌ Error: ai_soul_id is required, e.g.: aisoulhub install <ai_soul_id>'));
13
- process.exit(1);
14
- }
15
-
16
- // 1. 前置检查
17
- checkOpenclaw();
18
- await checkLogin();
19
-
20
- // 2. 获取当前的 agents 列表
21
- let agents = [];
22
- try {
23
- agents = getAgents();
24
- } catch (err) {
25
- console.error(chalk.yellow(`⚠️ Failed to get agents list: ${err.message}`));
26
- }
27
-
28
- // 3. 提示用户选择或创建 Agent
29
- const choices = [
30
- { name: '✨ Create new Agent', value: { name: 'create_new', path: null } },
31
- ...agents.map(a => ({ name: `📁 ${a.name} (${a.path})`, value: { name: a.name, path: a.path } }))
32
- ];
33
-
34
- const agentChoice = await select({
35
- message: 'Please select the Agent to install:',
36
- choices
37
- });
38
-
39
- if (agentChoice.name === 'main') {
40
- const isConfirmed = await confirm({
41
- message: chalk.yellow('⚠️ Warning: Overwriting the "main" agent might cause unexpected issues. Are you sure you want to continue?'),
42
- default: false
43
- });
44
-
45
- if (!isConfirmed) {
46
- console.log(chalk.blue('ℹ️ Operation cancelled by user.'));
47
- process.exit(0);
48
- }
49
- }
50
-
51
- let targetPath = agentChoice.path;
52
-
53
- if (agentChoice.name === 'create_new') {
54
- const newAgentName = await input({
55
- message: 'Please enter the name of the new Agent:',
56
- validate: (val) => val.trim().length > 0 ? true : 'Agent name cannot be empty'
57
- });
58
-
59
- const defaultWorkspace = path.join(os.homedir(), '.openclaw', `workspace-${newAgentName}`);
60
-
61
- const newWorkspacePath = await input({
62
- message: 'Please enter the workspace directory for the new Agent:',
63
- default: defaultWorkspace,
64
- validate: (val) => val.trim().length > 0 ? true : 'Workspace directory cannot be empty'
65
- });
66
-
67
- console.log(chalk.cyan(`🔄 Creating Agent [${newAgentName}]...`));
68
- try {
69
- addAgent(newAgentName, newWorkspacePath);
70
- console.log(chalk.green(`✅ Agent [${newAgentName}] created successfully.`));
71
-
72
- // 重新获取列表以获取新 agent 的路径
73
- const updatedAgents = getAgents();
74
- const newAgent = updatedAgents.find(a => a.name === newAgentName);
75
- if (!newAgent || !newAgent.path) {
76
- targetPath = newWorkspacePath;
77
- } else {
78
- targetPath = newAgent.path;
79
- }
80
- } catch (err) {
81
- console.error(chalk.red(err.message));
82
- process.exit(1);
83
- }
84
- }
85
-
86
- if (!targetPath) {
87
- console.error(chalk.red('❌ Error: Unable to determine the workspace directory path for the Agent.'));
88
- process.exit(1);
89
- }
90
-
91
- console.log(chalk.blue(`ℹ️ Target workspace directory: ${targetPath}`));
92
-
93
- // 4. 下载并解压 ZIP 文件
94
- const downloadSuccess = await downloadAndExtract(aiSoulId, targetPath);
95
- if (!downloadSuccess) {
96
- process.exit(1);
97
- }
98
-
99
- // 5. 解析 aisoul-meta.json 并安装技能
100
- await processMetaJson(targetPath);
101
-
102
- console.log(chalk.green(`🎉 Successfully installed ${aiSoulId} to the Agent.`));
103
- }
@@ -1,90 +0,0 @@
1
- import { checkOpenclaw, checkLogin } from '../utils/check.js';
2
- import { getAgents } from '../utils/openclaw.js';
3
- import { downloadAndExtract } from '../utils/download.js';
4
- import { processMetaJson } from '../utils/meta.js';
5
- import { select, input, confirm } from '@inquirer/prompts';
6
- import fs from 'fs';
7
- import path from 'path';
8
- import chalk from 'chalk';
9
-
10
- export async function updateCommand() {
11
- // 1. 前置检查
12
- checkOpenclaw();
13
- await checkLogin();
14
-
15
- // 2. 获取当前的 agents 列表
16
- let agents = [];
17
- try {
18
- agents = getAgents();
19
- } catch (err) {
20
- console.error(chalk.red(`❌ Failed to get agents list: ${err.message}`));
21
- process.exit(1);
22
- }
23
-
24
- if (agents.length === 0) {
25
- console.error(chalk.yellow('⚠️ No agents available to update. Please install one first using the install command.'));
26
- process.exit(1);
27
- }
28
-
29
- // 3. 提示用户选择 Agent
30
- const choices = agents.map(a => ({ name: `📁 ${a.name} (${a.path})`, value: { name: a.name, path: a.path } }));
31
-
32
- const agentChoice = await select({
33
- message: 'Please select the Agent to update:',
34
- choices
35
- });
36
-
37
- if (agentChoice.name === 'main') {
38
- const isConfirmed = await confirm({
39
- message: chalk.yellow('⚠️ Warning: Overwriting the "main" agent might cause unexpected issues. Are you sure you want to continue?'),
40
- default: false
41
- });
42
-
43
- if (!isConfirmed) {
44
- console.log(chalk.blue('ℹ️ Operation cancelled by user.'));
45
- process.exit(0);
46
- }
47
- }
48
-
49
- const targetPath = agentChoice.path;
50
-
51
- if (!targetPath || !fs.existsSync(targetPath)) {
52
- console.error(chalk.red('❌ Error: Invalid Agent workspace directory.'));
53
- process.exit(1);
54
- }
55
-
56
- // 4. 确定 ai_soul_id
57
- let aiSoulId = null;
58
- const metaPath = path.join(targetPath, 'aisoul-meta.json');
59
- if (fs.existsSync(metaPath)) {
60
- try {
61
- const content = fs.readFileSync(metaPath, 'utf-8');
62
- const meta = JSON.parse(content);
63
- if (meta.ai_soul_id) {
64
- aiSoulId = meta.ai_soul_id;
65
- console.log(chalk.blue(`ℹ️ Detected ai_soul_id from aisoul-meta.json: ${aiSoulId}`));
66
- }
67
- } catch (e) {
68
- console.warn(chalk.yellow('⚠️ Unable to parse aisoul-meta.json.'));
69
- }
70
- }
71
-
72
- if (!aiSoulId) {
73
- console.log(chalk.yellow('ℹ️ Unable to get ai_soul_id from the current Agent\'s aisoul-meta.json.'));
74
- aiSoulId = await input({
75
- message: 'Please enter the ai_soul_id to update:',
76
- validate: (val) => val.trim().length > 0 ? true : 'ai_soul_id cannot be empty'
77
- });
78
- }
79
-
80
- // 5. 下载并解压 ZIP 文件 (覆盖)
81
- const downloadSuccess = await downloadAndExtract(aiSoulId, targetPath);
82
- if (!downloadSuccess) {
83
- process.exit(1);
84
- }
85
-
86
- // 6. 解析 aisoul-meta.json 并更新技能
87
- await processMetaJson(targetPath);
88
-
89
- console.log(chalk.green(`🎉 Successfully updated Agent [${aiSoulId}].`));
90
- }
@@ -1,53 +0,0 @@
1
- import shell from 'shelljs';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import chalk from 'chalk';
6
- import { input } from '@inquirer/prompts';
7
-
8
- export function checkOpenclaw() {
9
- if (!shell.which('openclaw')) {
10
- console.error(chalk.red('❌ Error: openclaw command not found. Please follow the instructions at https://openclaw.ai to install openclaw, then try again.'));
11
- process.exit(1);
12
- }
13
- }
14
-
15
- const CONFIG_PATH = path.join(os.homedir(), '.aisoulhub.json');
16
-
17
- export async function checkLogin() {
18
- let config = {};
19
- if (fs.existsSync(CONFIG_PATH)) {
20
- try {
21
- const content = fs.readFileSync(CONFIG_PATH, 'utf-8');
22
- config = JSON.parse(content);
23
- } catch (e) {
24
- // ignore parse error
25
- }
26
- }
27
-
28
- if (!config.email) {
29
- console.log(chalk.yellow('ℹ️ No login information detected. Please enter your email to login:'));
30
- const email = await input({
31
- message: 'Please enter your email address:',
32
- validate: (value) => {
33
- const pass = value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
34
- if (pass) {
35
- return true;
36
- }
37
- return 'Please enter a valid email address';
38
- }
39
- });
40
-
41
- config.email = email;
42
- try {
43
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
44
- console.log(chalk.green(`✅ Login successful! Email: ${email} saved.`));
45
- } catch (e) {
46
- console.log(chalk.yellow(`⚠️ Login successful, but unable to save to ${CONFIG_PATH}: ${e.message}`));
47
- }
48
- } else {
49
- console.log(chalk.blue(`ℹ️ Currently logged in as: ${config.email}`));
50
- }
51
-
52
- return config.email;
53
- }
@@ -1,61 +0,0 @@
1
- import axios from 'axios';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import unzipper from 'unzipper';
6
- import ora from 'ora';
7
- import chalk from 'chalk';
8
-
9
- export async function downloadAndExtract(aiSoulId, targetPath) {
10
- const url = `https://aisoulhub.com/packages/${aiSoulId}.zip`;
11
- const tempDir = os.tmpdir();
12
- const zipPath = path.join(tempDir, `${aiSoulId}.zip`);
13
-
14
- const spinner = ora(`Downloading package ${aiSoulId}...`).start();
15
-
16
- try {
17
- const response = await axios({
18
- method: 'GET',
19
- url,
20
- responseType: 'stream',
21
- // Provide a mock user-agent just in case
22
- headers: { 'User-Agent': 'aisoulhub-cli/1.0.0' }
23
- });
24
-
25
- const writer = fs.createWriteStream(zipPath);
26
- response.data.pipe(writer);
27
-
28
- await new Promise((resolve, reject) => {
29
- writer.on('finish', resolve);
30
- writer.on('error', reject);
31
- });
32
-
33
- spinner.text = `Download complete, extracting...`;
34
-
35
- // Ensure target path exists
36
- if (!fs.existsSync(targetPath)) {
37
- fs.mkdirSync(targetPath, { recursive: true });
38
- }
39
-
40
- // Unzip
41
- await fs.createReadStream(zipPath)
42
- .pipe(unzipper.Extract({ path: targetPath }))
43
- .promise();
44
-
45
- spinner.succeed(chalk.green(`Package ${aiSoulId} downloaded and extracted successfully.`));
46
-
47
- // Cleanup temp zip
48
- fs.unlinkSync(zipPath);
49
- return true;
50
- } catch (err) {
51
- if (err.response && err.response.status === 404) {
52
- spinner.fail(chalk.red(`Error: Package not found. Please check if the ai_soul_id [${aiSoulId}] is correct.`));
53
- } else {
54
- spinner.fail(chalk.red(`Error: Failed to download or extract the package: ${err.message}`));
55
- }
56
- if (fs.existsSync(zipPath)) {
57
- fs.unlinkSync(zipPath);
58
- }
59
- return false;
60
- }
61
- }
package/src/utils/meta.js DELETED
@@ -1,37 +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
-
23
- console.log(chalk.cyan(`🔄 Starting installation of ${skills.length} skills...`));
24
- for (const skill of skills) {
25
- console.log(chalk.blue(`⏳ Installing skill: ${skill}...`));
26
- const success = await installSkill(workspacePath, skill);
27
- if (success) {
28
- console.log(chalk.green(`✅ Skill ${skill} installed successfully.`));
29
- } else {
30
- console.log(chalk.red(`❌ Failed to install skill ${skill}.`));
31
- }
32
- }
33
- console.log(chalk.green('🎉 All skills installation processes completed.'));
34
- } catch (err) {
35
- console.error(chalk.red(`❌ Failed to read or parse aisoul-meta.json: ${err.message}`));
36
- }
37
- }
@@ -1,74 +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
- const list = JSON.parse(result.stdout);
12
- if (Array.isArray(list)) {
13
- return list.map(item => ({
14
- name: item.id || item.name,
15
- path: item.workspace || item.path || item.agentDir
16
- }));
17
- }
18
- } catch (err) {
19
- // Not JSON, parse text
20
- }
21
-
22
- // Fallback text parsing
23
- const lines = result.stdout.split('\n');
24
- const agents = [];
25
-
26
- // Regex to match agent name and path. E.g., "- agent1 (/path/to/agent1)" or "agent1: /path/to/agent1"
27
- // Let's just do a generic parsing or return raw lines if we can't be sure.
28
- for (const line of lines) {
29
- const match = line.match(/(?:-\s+)?(\w+)(?:\s+\((.*?)\)|:\s+(.*))?/);
30
- if (match && match[1] && match[1] !== 'Agents') {
31
- const name = match[1].trim();
32
- const pathStr = (match[2] || match[3] || '').trim();
33
- if (name) {
34
- agents.push({ name, path: pathStr || null });
35
- }
36
- }
37
- }
38
- return agents;
39
- }
40
-
41
- export function addAgent(name, workspacePath) {
42
- let cmd = `openclaw agents add "${name}"`;
43
- if (workspacePath) {
44
- cmd += ` --non-interactive --workspace "${workspacePath}"`;
45
- }
46
- const result = shell.exec(cmd, { silent: true });
47
- if (result.code !== 0) {
48
- throw new Error(`Failed to create agent [${name}]: ` + result.stderr);
49
- }
50
- return result.stdout;
51
- }
52
-
53
- export async function installSkill(workspacePath, skillStr, retryCount = 0) {
54
- const maxRetries = 3;
55
- const result = shell.exec(`npx clawhub install ${skillStr} --workdir "${workspacePath}"`, { silent: true });
56
-
57
- if (result.code !== 0) {
58
- const errorOutput = result.stderr || result.stdout;
59
- if (errorOutput.includes('Rate limit exceeded')) {
60
- if (retryCount < maxRetries) {
61
- console.log(`⚠️ Rate limit exceeded. Waiting 5 seconds before retrying (${retryCount + 1}/${maxRetries})...`);
62
- await new Promise(resolve => setTimeout(resolve, 5000));
63
- return installSkill(workspacePath, skillStr, retryCount + 1);
64
- } else {
65
- console.error(`❌ Failed to install skill ${skillStr} after ${maxRetries} retries due to rate limit.`);
66
- return false;
67
- }
68
- }
69
-
70
- console.error(`❌ Failed to install skill ${skillStr}: `, errorOutput);
71
- return false;
72
- }
73
- return true;
74
- }