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 +34 -30
- package/bin/aisoulhub.js +5 -1
- package/package.json +3 -5
- package/src/index.js +532 -20
- package/src/commands/install.js +0 -103
- package/src/commands/update.js +0 -90
- package/src/utils/check.js +0 -53
- package/src/utils/download.js +0 -61
- package/src/utils/meta.js +0 -37
- package/src/utils/openclaw.js +0 -74
package/README.md
CHANGED
|
@@ -1,54 +1,58 @@
|
|
|
1
1
|
# AISoulHub CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`aisoulhub` 是一个用于安装 aisoul 的命令行工具,可将 aisoul 安装到本地 OpenClaw workspace 中。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 环境要求
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- [Node.js](https://nodejs.org/) v16 或更高版本
|
|
8
|
+
- 已安装并可直接执行的 `openclaw`
|
|
9
|
+
- 可通过 `npx` 执行 `clawhub`
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 使用方式
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
### 安装 aisoul
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npx aisoulhub install <slug[@version]> --user <your@email.com>
|
|
17
|
+
```
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
示例:
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
npx aisoulhub install
|
|
22
|
+
npx aisoulhub install helloworld --user your@gmail.com
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
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
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npx aisoulhub install helloworld@1.0.2 --user your@gmail.com
|
|
29
|
+
```
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
使用自定义服务地址:
|
|
34
32
|
|
|
35
33
|
```bash
|
|
36
|
-
npx aisoulhub
|
|
34
|
+
npx aisoulhub install helloworld --user your@gmail.com --base_url=https://aisoulhub.com
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
|
|
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
|
-
|
|
39
|
+
- `<slug[@version]>`:aisoul 标识,`@version` 可选
|
|
40
|
+
- `--user`:请使用你的注册邮箱
|
|
41
|
+
- `--base_url`:可选,自定义服务地址
|
|
47
42
|
|
|
48
|
-
|
|
43
|
+
安装提示:
|
|
49
44
|
|
|
50
|
-
|
|
45
|
+
- 如果未指定版本,将自动安装该 aisoul 的最新版本
|
|
46
|
+
- 如果已存在同名 agent,请先执行 `openclaw agents delete <agentName>` 后再重新安装
|
|
51
47
|
|
|
52
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aisoulhub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
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
|
|
3
|
-
import {
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
|
9
|
-
const
|
|
13
|
+
export const DEFAULT_BASE_URL = 'https://aisouhub.com';
|
|
14
|
+
const CLAWHUB_REGISTRY = 'https://cn.clawhub-mirror.com';
|
|
10
15
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
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('
|
|
20
|
-
.
|
|
524
|
+
.description('安装和更新 aisoul 到 OpenClaw workspace')
|
|
525
|
+
.showHelpAfterError();
|
|
21
526
|
|
|
22
527
|
program
|
|
23
528
|
.command('install')
|
|
24
|
-
.description('
|
|
25
|
-
.argument('<
|
|
26
|
-
.
|
|
27
|
-
|
|
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('
|
|
33
|
-
.action(
|
|
34
|
-
|
|
543
|
+
.description('预留的更新命令')
|
|
544
|
+
.action(() => {
|
|
545
|
+
console.error('update 命令暂未实现');
|
|
546
|
+
process.exit(1);
|
|
35
547
|
});
|
|
36
548
|
|
|
37
|
-
program.parseAsync(
|
|
549
|
+
await program.parseAsync(argv);
|
|
38
550
|
}
|
package/src/commands/install.js
DELETED
|
@@ -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
|
-
}
|
package/src/commands/update.js
DELETED
|
@@ -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
|
-
}
|
package/src/utils/check.js
DELETED
|
@@ -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
|
-
}
|
package/src/utils/download.js
DELETED
|
@@ -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
|
-
}
|
package/src/utils/openclaw.js
DELETED
|
@@ -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
|
-
}
|