soulhubcli 1.0.7 → 1.0.9
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/dist/index.cjs +17570 -0
- package/dist/index.d.cts +2 -0
- package/package.json +4 -6
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1620
package/dist/index.js
DELETED
|
@@ -1,1620 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/index.ts
|
|
4
|
-
import { Command as Command8 } from "commander";
|
|
5
|
-
|
|
6
|
-
// src/commands/search.ts
|
|
7
|
-
import { Command } from "commander";
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
|
|
10
|
-
// src/utils.ts
|
|
11
|
-
import fs2 from "fs";
|
|
12
|
-
import path2 from "path";
|
|
13
|
-
import { execSync } from "child_process";
|
|
14
|
-
import yaml from "js-yaml";
|
|
15
|
-
|
|
16
|
-
// src/logger.ts
|
|
17
|
-
import fs from "fs";
|
|
18
|
-
import path from "path";
|
|
19
|
-
var LEVEL_PRIORITY = {
|
|
20
|
-
debug: 0,
|
|
21
|
-
info: 1,
|
|
22
|
-
warn: 2,
|
|
23
|
-
error: 3
|
|
24
|
-
};
|
|
25
|
-
var LOG_RETENTION_DAYS = 7;
|
|
26
|
-
var Logger = class {
|
|
27
|
-
logDir;
|
|
28
|
-
verbose = false;
|
|
29
|
-
initialized = false;
|
|
30
|
-
constructor() {
|
|
31
|
-
const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
32
|
-
this.logDir = path.join(home, ".soulhub", "logs");
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* 初始化日志系统
|
|
36
|
-
* @param verbose 是否开启 debug 级别输出到终端
|
|
37
|
-
*/
|
|
38
|
-
init(verbose = false) {
|
|
39
|
-
this.verbose = verbose;
|
|
40
|
-
if (!fs.existsSync(this.logDir)) {
|
|
41
|
-
fs.mkdirSync(this.logDir, { recursive: true });
|
|
42
|
-
}
|
|
43
|
-
this.initialized = true;
|
|
44
|
-
this.cleanOldLogs();
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* 获取当前日志文件路径(按日期)
|
|
48
|
-
*/
|
|
49
|
-
getLogFilePath() {
|
|
50
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
51
|
-
return path.join(this.logDir, `soulhub-${date}.log`);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* 格式化日志行
|
|
55
|
-
*/
|
|
56
|
-
formatLine(level, message, meta) {
|
|
57
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
58
|
-
const levelTag = level.toUpperCase().padEnd(5);
|
|
59
|
-
let line = `[${timestamp}] ${levelTag} ${message}`;
|
|
60
|
-
if (meta && Object.keys(meta).length > 0) {
|
|
61
|
-
line += ` | ${JSON.stringify(meta)}`;
|
|
62
|
-
}
|
|
63
|
-
return line;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* 写入日志到文件
|
|
67
|
-
*/
|
|
68
|
-
write(level, message, meta) {
|
|
69
|
-
if (!this.initialized) {
|
|
70
|
-
try {
|
|
71
|
-
this.init(this.verbose);
|
|
72
|
-
} catch {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const line = this.formatLine(level, message, meta);
|
|
77
|
-
try {
|
|
78
|
-
fs.appendFileSync(this.getLogFilePath(), line + "\n");
|
|
79
|
-
} catch {
|
|
80
|
-
}
|
|
81
|
-
if (this.verbose && LEVEL_PRIORITY[level] === LEVEL_PRIORITY.debug) {
|
|
82
|
-
console.log(` ${line}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* 清理超过保留天数的旧日志
|
|
87
|
-
*/
|
|
88
|
-
cleanOldLogs() {
|
|
89
|
-
try {
|
|
90
|
-
const files = fs.readdirSync(this.logDir);
|
|
91
|
-
const now = Date.now();
|
|
92
|
-
const maxAge = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
93
|
-
for (const file of files) {
|
|
94
|
-
if (!file.startsWith("soulhub-") || !file.endsWith(".log")) continue;
|
|
95
|
-
const filePath = path.join(this.logDir, file);
|
|
96
|
-
const stat = fs.statSync(filePath);
|
|
97
|
-
if (now - stat.mtimeMs > maxAge) {
|
|
98
|
-
fs.unlinkSync(filePath);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} catch {
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// ==========================================
|
|
105
|
-
// 公开的日志方法
|
|
106
|
-
// ==========================================
|
|
107
|
-
debug(message, meta) {
|
|
108
|
-
this.write("debug", message, meta);
|
|
109
|
-
}
|
|
110
|
-
info(message, meta) {
|
|
111
|
-
this.write("info", message, meta);
|
|
112
|
-
}
|
|
113
|
-
warn(message, meta) {
|
|
114
|
-
this.write("warn", message, meta);
|
|
115
|
-
}
|
|
116
|
-
error(message, meta) {
|
|
117
|
-
this.write("error", message, meta);
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* 记录错误对象(自动提取 message 和 stack)
|
|
121
|
-
*/
|
|
122
|
-
errorObj(message, err) {
|
|
123
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
124
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
125
|
-
this.write("error", message, { error: errMsg, stack });
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* 获取日志目录路径(供用户查看)
|
|
129
|
-
*/
|
|
130
|
-
getLogDir() {
|
|
131
|
-
return this.logDir;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* 获取今天的日志文件路径
|
|
135
|
-
*/
|
|
136
|
-
getTodayLogFile() {
|
|
137
|
-
return this.getLogFilePath();
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
var logger = new Logger();
|
|
141
|
-
|
|
142
|
-
// src/utils.ts
|
|
143
|
-
var DEFAULT_REGISTRY_URL = "http://soulhub.store";
|
|
144
|
-
function getRegistryUrl() {
|
|
145
|
-
return process.env.SOULHUB_REGISTRY_URL || DEFAULT_REGISTRY_URL;
|
|
146
|
-
}
|
|
147
|
-
async function fetchIndex() {
|
|
148
|
-
const url = `${getRegistryUrl()}/index.json`;
|
|
149
|
-
logger.debug(`Fetching registry index`, { url });
|
|
150
|
-
const response = await fetch(url);
|
|
151
|
-
if (!response.ok) {
|
|
152
|
-
logger.error(`Failed to fetch registry index`, { url, status: response.status, statusText: response.statusText });
|
|
153
|
-
throw new Error(`Failed to fetch registry index: ${response.statusText}`);
|
|
154
|
-
}
|
|
155
|
-
return await response.json();
|
|
156
|
-
}
|
|
157
|
-
async function fetchAgentFile(agentName, fileName) {
|
|
158
|
-
const url = `${getRegistryUrl()}/agents/${agentName}/${fileName}`;
|
|
159
|
-
logger.debug(`Fetching agent file`, { agentName, fileName, url });
|
|
160
|
-
const response = await fetch(url);
|
|
161
|
-
if (!response.ok) {
|
|
162
|
-
logger.error(`Failed to fetch agent file`, { agentName, fileName, url, status: response.status });
|
|
163
|
-
throw new Error(
|
|
164
|
-
`Failed to fetch ${fileName} for ${agentName}: ${response.statusText}`
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
return await response.text();
|
|
168
|
-
}
|
|
169
|
-
async function fetchRecipeFile(recipeName, fileName) {
|
|
170
|
-
const url = `${getRegistryUrl()}/recipes/${recipeName}/${fileName}`;
|
|
171
|
-
logger.debug(`Fetching recipe file`, { recipeName, fileName, url });
|
|
172
|
-
const response = await fetch(url);
|
|
173
|
-
if (!response.ok) {
|
|
174
|
-
logger.error(`Failed to fetch recipe file`, { recipeName, fileName, url, status: response.status });
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Failed to fetch ${fileName} for recipe ${recipeName}: ${response.statusText}`
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
return await response.text();
|
|
180
|
-
}
|
|
181
|
-
function findOpenClawDir(customDir) {
|
|
182
|
-
if (customDir) {
|
|
183
|
-
const resolved = path2.resolve(customDir);
|
|
184
|
-
if (fs2.existsSync(resolved)) {
|
|
185
|
-
return resolved;
|
|
186
|
-
}
|
|
187
|
-
return resolved;
|
|
188
|
-
}
|
|
189
|
-
const envHome = process.env.OPENCLAW_HOME;
|
|
190
|
-
if (envHome) {
|
|
191
|
-
const resolved = path2.resolve(envHome);
|
|
192
|
-
if (fs2.existsSync(resolved)) {
|
|
193
|
-
return resolved;
|
|
194
|
-
}
|
|
195
|
-
return resolved;
|
|
196
|
-
}
|
|
197
|
-
const candidates = [
|
|
198
|
-
path2.join(process.env.HOME || "~", ".openclaw"),
|
|
199
|
-
path2.join(process.cwd(), ".openclaw")
|
|
200
|
-
];
|
|
201
|
-
for (const candidate of candidates) {
|
|
202
|
-
if (fs2.existsSync(candidate)) {
|
|
203
|
-
return candidate;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
function getConfigPath() {
|
|
209
|
-
const home = process.env.HOME || "~";
|
|
210
|
-
return path2.join(home, ".soulhub", "config.json");
|
|
211
|
-
}
|
|
212
|
-
function loadConfig() {
|
|
213
|
-
const configPath = getConfigPath();
|
|
214
|
-
if (fs2.existsSync(configPath)) {
|
|
215
|
-
return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
216
|
-
}
|
|
217
|
-
return {
|
|
218
|
-
installed: [],
|
|
219
|
-
registryUrl: DEFAULT_REGISTRY_URL
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
function saveConfig(config) {
|
|
223
|
-
const configPath = getConfigPath();
|
|
224
|
-
const configDir = path2.dirname(configPath);
|
|
225
|
-
if (!fs2.existsSync(configDir)) {
|
|
226
|
-
fs2.mkdirSync(configDir, { recursive: true });
|
|
227
|
-
}
|
|
228
|
-
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
229
|
-
}
|
|
230
|
-
function recordInstall(name, version, workspace) {
|
|
231
|
-
const config = loadConfig();
|
|
232
|
-
config.installed = config.installed.filter((a) => a.name !== name);
|
|
233
|
-
config.installed.push({
|
|
234
|
-
name,
|
|
235
|
-
version,
|
|
236
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
237
|
-
workspace
|
|
238
|
-
});
|
|
239
|
-
saveConfig(config);
|
|
240
|
-
logger.debug(`Recorded install`, { name, version, workspace });
|
|
241
|
-
}
|
|
242
|
-
function removeInstallRecord(name) {
|
|
243
|
-
const config = loadConfig();
|
|
244
|
-
config.installed = config.installed.filter((a) => a.name !== name);
|
|
245
|
-
saveConfig(config);
|
|
246
|
-
}
|
|
247
|
-
function getWorkspaceDir(clawDir, agentName) {
|
|
248
|
-
return path2.join(clawDir, `workspace-${agentName}`);
|
|
249
|
-
}
|
|
250
|
-
function getMainWorkspaceDir(clawDir) {
|
|
251
|
-
return path2.join(clawDir, "workspace");
|
|
252
|
-
}
|
|
253
|
-
function checkMainAgentExists(clawDir) {
|
|
254
|
-
const workspaceDir = getMainWorkspaceDir(clawDir);
|
|
255
|
-
if (!fs2.existsSync(workspaceDir)) {
|
|
256
|
-
return { exists: false, hasContent: false, workspaceDir };
|
|
257
|
-
}
|
|
258
|
-
const entries = fs2.readdirSync(workspaceDir);
|
|
259
|
-
const hasIdentity = entries.includes("IDENTITY.md");
|
|
260
|
-
const hasSoul = entries.includes("SOUL.md");
|
|
261
|
-
return {
|
|
262
|
-
exists: true,
|
|
263
|
-
hasContent: hasIdentity || hasSoul,
|
|
264
|
-
workspaceDir
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function getOpenClawConfigPath(clawDir) {
|
|
268
|
-
return path2.join(clawDir, "openclaw.json");
|
|
269
|
-
}
|
|
270
|
-
function readOpenClawConfig(clawDir) {
|
|
271
|
-
const configPath = getOpenClawConfigPath(clawDir);
|
|
272
|
-
if (!fs2.existsSync(configPath)) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
277
|
-
} catch {
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
function writeOpenClawConfig(clawDir, config) {
|
|
282
|
-
const configPath = getOpenClawConfigPath(clawDir);
|
|
283
|
-
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
284
|
-
}
|
|
285
|
-
function updateOpenClawConfig(clawDir, updater) {
|
|
286
|
-
const config = readOpenClawConfig(clawDir);
|
|
287
|
-
if (!config) {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
const updated = updater(config);
|
|
291
|
-
writeOpenClawConfig(clawDir, updated);
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
function configureMultiAgentCommunication(clawDir, dispatcherId, workerIds) {
|
|
295
|
-
return updateOpenClawConfig(clawDir, (config) => {
|
|
296
|
-
if (!config.agents) config.agents = {};
|
|
297
|
-
if (!config.agents.list) config.agents.list = [];
|
|
298
|
-
const dispatcherAgent = config.agents.list.find((a) => a.id === dispatcherId);
|
|
299
|
-
if (dispatcherAgent) {
|
|
300
|
-
dispatcherAgent.subagents = {
|
|
301
|
-
...dispatcherAgent.subagents,
|
|
302
|
-
allowAgents: workerIds
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
if (!config.tools) config.tools = {};
|
|
306
|
-
config.tools.sessions = {
|
|
307
|
-
...config.tools.sessions,
|
|
308
|
-
visibility: "all"
|
|
309
|
-
};
|
|
310
|
-
const allAgentIds = [dispatcherId, ...workerIds];
|
|
311
|
-
config.tools.agentToAgent = {
|
|
312
|
-
enabled: true,
|
|
313
|
-
allow: allAgentIds
|
|
314
|
-
};
|
|
315
|
-
return config;
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
function addAgentToOpenClawConfig(clawDir, agentId, agentName, isMain) {
|
|
319
|
-
return updateOpenClawConfig(clawDir, (config) => {
|
|
320
|
-
if (!config.agents) config.agents = {};
|
|
321
|
-
if (!config.agents.list) config.agents.list = [];
|
|
322
|
-
const existing = config.agents.list.find((a) => a.id === agentId);
|
|
323
|
-
if (existing) {
|
|
324
|
-
existing.name = agentName;
|
|
325
|
-
return config;
|
|
326
|
-
}
|
|
327
|
-
if (isMain) {
|
|
328
|
-
config.agents.list.push({
|
|
329
|
-
id: agentId,
|
|
330
|
-
name: agentName
|
|
331
|
-
});
|
|
332
|
-
} else {
|
|
333
|
-
config.agents.list.push({
|
|
334
|
-
id: agentId,
|
|
335
|
-
name: agentName,
|
|
336
|
-
workspace: path2.join(clawDir, `workspace-${agentId}`),
|
|
337
|
-
agentDir: path2.join(clawDir, `agents/${agentId}/agent`)
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
return config;
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
function readSoulHubPackage(dir) {
|
|
344
|
-
const yamlPath = path2.join(dir, "soulhub.yaml");
|
|
345
|
-
if (!fs2.existsSync(yamlPath)) {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
return yaml.load(fs2.readFileSync(yamlPath, "utf-8"));
|
|
350
|
-
} catch {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
function detectPackageKind(dir) {
|
|
355
|
-
const pkg = readSoulHubPackage(dir);
|
|
356
|
-
if (pkg) {
|
|
357
|
-
return pkg.kind;
|
|
358
|
-
}
|
|
359
|
-
if (fs2.existsSync(path2.join(dir, "IDENTITY.md"))) {
|
|
360
|
-
return "agent";
|
|
361
|
-
}
|
|
362
|
-
return "unknown";
|
|
363
|
-
}
|
|
364
|
-
function checkOpenClawInstalled(customDir) {
|
|
365
|
-
const clawDir = findOpenClawDir(customDir);
|
|
366
|
-
if (clawDir) {
|
|
367
|
-
return {
|
|
368
|
-
installed: true,
|
|
369
|
-
clawDir,
|
|
370
|
-
message: `OpenClaw detected at: ${clawDir}`
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
try {
|
|
374
|
-
execSync("which openclaw 2>/dev/null || where openclaw 2>nul", {
|
|
375
|
-
stdio: "pipe"
|
|
376
|
-
});
|
|
377
|
-
return {
|
|
378
|
-
installed: true,
|
|
379
|
-
clawDir: null,
|
|
380
|
-
message: "OpenClaw command found in PATH, but workspace directory not detected."
|
|
381
|
-
};
|
|
382
|
-
} catch {
|
|
383
|
-
}
|
|
384
|
-
return {
|
|
385
|
-
installed: false,
|
|
386
|
-
clawDir: null,
|
|
387
|
-
message: "OpenClaw is not installed. Please install OpenClaw first, use --claw-dir to specify OpenClaw directory, or set OPENCLAW_HOME environment variable."
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
function backupAgentWorkspace(workspaceDir) {
|
|
391
|
-
if (!fs2.existsSync(workspaceDir)) {
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
const entries = fs2.readdirSync(workspaceDir);
|
|
395
|
-
if (entries.length === 0) {
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
const clawDir = path2.dirname(workspaceDir);
|
|
399
|
-
const backupBaseDir = path2.join(clawDir, "agentbackup");
|
|
400
|
-
if (!fs2.existsSync(backupBaseDir)) {
|
|
401
|
-
fs2.mkdirSync(backupBaseDir, { recursive: true });
|
|
402
|
-
}
|
|
403
|
-
const dirName = path2.basename(workspaceDir);
|
|
404
|
-
let backupDir = path2.join(backupBaseDir, dirName);
|
|
405
|
-
if (fs2.existsSync(backupDir)) {
|
|
406
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
407
|
-
backupDir = path2.join(backupBaseDir, `${dirName}-${timestamp}`);
|
|
408
|
-
}
|
|
409
|
-
fs2.cpSync(workspaceDir, backupDir, { recursive: true });
|
|
410
|
-
logger.info(`Workspace backed up`, { from: workspaceDir, to: backupDir });
|
|
411
|
-
return backupDir;
|
|
412
|
-
}
|
|
413
|
-
function moveBackupAgentWorkspace(workspaceDir) {
|
|
414
|
-
if (!fs2.existsSync(workspaceDir)) {
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
417
|
-
const entries = fs2.readdirSync(workspaceDir);
|
|
418
|
-
if (entries.length === 0) {
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
const clawDir = path2.dirname(workspaceDir);
|
|
422
|
-
const backupBaseDir = path2.join(clawDir, "agentbackup");
|
|
423
|
-
if (!fs2.existsSync(backupBaseDir)) {
|
|
424
|
-
fs2.mkdirSync(backupBaseDir, { recursive: true });
|
|
425
|
-
}
|
|
426
|
-
const dirName = path2.basename(workspaceDir);
|
|
427
|
-
let backupDir = path2.join(backupBaseDir, dirName);
|
|
428
|
-
if (fs2.existsSync(backupDir)) {
|
|
429
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
430
|
-
backupDir = path2.join(backupBaseDir, `${dirName}-${timestamp}`);
|
|
431
|
-
}
|
|
432
|
-
fs2.renameSync(workspaceDir, backupDir);
|
|
433
|
-
logger.info(`Workspace moved to backup`, { from: workspaceDir, to: backupDir });
|
|
434
|
-
return backupDir;
|
|
435
|
-
}
|
|
436
|
-
function backupAllWorkerWorkspaces(clawDir) {
|
|
437
|
-
const results = [];
|
|
438
|
-
const workerDirs = listAgentWorkspaces(clawDir);
|
|
439
|
-
for (const dirName of workerDirs) {
|
|
440
|
-
const fullPath = path2.join(clawDir, dirName);
|
|
441
|
-
const backupDir = moveBackupAgentWorkspace(fullPath);
|
|
442
|
-
if (backupDir) {
|
|
443
|
-
results.push({ name: dirName, backupDir });
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return results;
|
|
447
|
-
}
|
|
448
|
-
function listAgentWorkspaces(clawDir) {
|
|
449
|
-
if (!fs2.existsSync(clawDir)) {
|
|
450
|
-
return [];
|
|
451
|
-
}
|
|
452
|
-
return fs2.readdirSync(clawDir).filter((entry) => {
|
|
453
|
-
const fullPath = path2.join(clawDir, entry);
|
|
454
|
-
return fs2.statSync(fullPath).isDirectory() && entry.startsWith("workspace-");
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
function getBackupManifestPath() {
|
|
458
|
-
const home = process.env.HOME || "~";
|
|
459
|
-
return path2.join(home, ".soulhub", "backups.json");
|
|
460
|
-
}
|
|
461
|
-
function loadBackupManifest() {
|
|
462
|
-
const manifestPath = getBackupManifestPath();
|
|
463
|
-
if (fs2.existsSync(manifestPath)) {
|
|
464
|
-
try {
|
|
465
|
-
return JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
466
|
-
} catch {
|
|
467
|
-
return { records: [] };
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return { records: [] };
|
|
471
|
-
}
|
|
472
|
-
function saveBackupManifest(manifest) {
|
|
473
|
-
const manifestPath = getBackupManifestPath();
|
|
474
|
-
const manifestDir = path2.dirname(manifestPath);
|
|
475
|
-
if (!fs2.existsSync(manifestDir)) {
|
|
476
|
-
fs2.mkdirSync(manifestDir, { recursive: true });
|
|
477
|
-
}
|
|
478
|
-
manifest.records = manifest.records.slice(0, 50);
|
|
479
|
-
fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
480
|
-
}
|
|
481
|
-
function generateBackupId() {
|
|
482
|
-
const now = /* @__PURE__ */ new Date();
|
|
483
|
-
const ts = now.toISOString().replace(/[:.\-T]/g, "").slice(0, 14);
|
|
484
|
-
const rand = Math.random().toString(36).slice(2, 6);
|
|
485
|
-
return `${ts}-${rand}`;
|
|
486
|
-
}
|
|
487
|
-
function createBackupRecord(installType, packageName, clawDir) {
|
|
488
|
-
let openclawJsonSnapshot = null;
|
|
489
|
-
const configPath = path2.join(clawDir, "openclaw.json");
|
|
490
|
-
if (fs2.existsSync(configPath)) {
|
|
491
|
-
try {
|
|
492
|
-
openclawJsonSnapshot = fs2.readFileSync(configPath, "utf-8");
|
|
493
|
-
} catch {
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return {
|
|
497
|
-
id: generateBackupId(),
|
|
498
|
-
installType,
|
|
499
|
-
packageName,
|
|
500
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
501
|
-
clawDir,
|
|
502
|
-
openclawJsonSnapshot,
|
|
503
|
-
items: [],
|
|
504
|
-
installedWorkerIds: [],
|
|
505
|
-
installedMainAgent: null
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
function addBackupItem(record, item) {
|
|
509
|
-
record.items.push(item);
|
|
510
|
-
}
|
|
511
|
-
function commitBackupRecord(record) {
|
|
512
|
-
if (record.items.length === 0 && record.installedWorkerIds.length === 0 && !record.installedMainAgent) {
|
|
513
|
-
logger.debug("No backup items to record, skipping.");
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const manifest = loadBackupManifest();
|
|
517
|
-
manifest.records.unshift(record);
|
|
518
|
-
saveBackupManifest(manifest);
|
|
519
|
-
logger.info(`Backup record saved`, { id: record.id, items: record.items.length, workers: record.installedWorkerIds.length });
|
|
520
|
-
}
|
|
521
|
-
var CATEGORY_LABELS = {
|
|
522
|
-
"self-media": "Self Media",
|
|
523
|
-
development: "Development",
|
|
524
|
-
operations: "Operations",
|
|
525
|
-
support: "Support",
|
|
526
|
-
education: "Education",
|
|
527
|
-
dispatcher: "Dispatcher"
|
|
528
|
-
};
|
|
529
|
-
function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
|
|
530
|
-
const agentId = agentName.toLowerCase().replace(/[\s_]+/g, "-");
|
|
531
|
-
logger.debug(`Registering agent to OpenClaw`, { agentId, workspaceDir });
|
|
532
|
-
try {
|
|
533
|
-
execSync(
|
|
534
|
-
`openclaw agents add "${agentId}" --workspace "${workspaceDir}" --non-interactive --json`,
|
|
535
|
-
{ stdio: "pipe", timeout: 15e3 }
|
|
536
|
-
);
|
|
537
|
-
return {
|
|
538
|
-
success: true,
|
|
539
|
-
message: `Agent "${agentId}" registered via OpenClaw CLI.`
|
|
540
|
-
};
|
|
541
|
-
} catch (cliError) {
|
|
542
|
-
const stderr = cliError && typeof cliError === "object" && "stderr" in cliError ? String(cliError.stderr) : "";
|
|
543
|
-
if (stderr.includes("already exists")) {
|
|
544
|
-
return {
|
|
545
|
-
success: true,
|
|
546
|
-
message: `Agent "${agentId}" already registered in OpenClaw.`
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
const isCommandNotFound = cliError && typeof cliError === "object" && "code" in cliError && cliError.code === "ENOENT" || stderr.includes("not found") || stderr.includes("not recognized");
|
|
550
|
-
if (isCommandNotFound) {
|
|
551
|
-
logger.error(`OpenClaw CLI not found`);
|
|
552
|
-
return {
|
|
553
|
-
success: false,
|
|
554
|
-
message: "OpenClaw CLI not found. Please install OpenClaw first: https://github.com/anthropics/openclaw"
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
const errMsg = cliError instanceof Error ? cliError.message : String(cliError);
|
|
558
|
-
logger.error(`openclaw agents add failed`, { agentId, stderr, error: errMsg });
|
|
559
|
-
return {
|
|
560
|
-
success: false,
|
|
561
|
-
message: `openclaw agents add failed: ${stderr || errMsg}`
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function restartOpenClawGateway() {
|
|
566
|
-
logger.debug(`Restarting OpenClaw Gateway`);
|
|
567
|
-
try {
|
|
568
|
-
execSync("openclaw gateway restart", {
|
|
569
|
-
stdio: "pipe",
|
|
570
|
-
timeout: 3e4
|
|
571
|
-
// 30 秒超时
|
|
572
|
-
});
|
|
573
|
-
return {
|
|
574
|
-
success: true,
|
|
575
|
-
message: "OpenClaw Gateway restarted successfully."
|
|
576
|
-
};
|
|
577
|
-
} catch (error) {
|
|
578
|
-
const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr).trim() : "";
|
|
579
|
-
const errMsg = stderr || (error instanceof Error ? error.message : String(error));
|
|
580
|
-
return {
|
|
581
|
-
success: false,
|
|
582
|
-
message: errMsg
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// src/commands/search.ts
|
|
588
|
-
var searchCommand = new Command("search").description("Search for agent templates in the SoulHub registry").argument("[query]", "Search query (matches name, description, tags)").option("-c, --category <category>", "Filter by category").option("-l, --limit <number>", "Max results to show", "20").action(async (query, options) => {
|
|
589
|
-
try {
|
|
590
|
-
const index = await fetchIndex();
|
|
591
|
-
let agents = index.agents;
|
|
592
|
-
if (options.category) {
|
|
593
|
-
agents = agents.filter(
|
|
594
|
-
(a) => a.category === options.category
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
if (query) {
|
|
598
|
-
const q = query.toLowerCase();
|
|
599
|
-
agents = agents.filter(
|
|
600
|
-
(a) => a.name.toLowerCase().includes(q) || a.displayName.toLowerCase().includes(q) || a.description.toLowerCase().includes(q) || a.tags.some((t) => t.toLowerCase().includes(q))
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
const limit = parseInt(options.limit, 10);
|
|
604
|
-
const shown = agents.slice(0, limit);
|
|
605
|
-
if (shown.length === 0) {
|
|
606
|
-
console.log(chalk.yellow("No agents found matching your query."));
|
|
607
|
-
if (query) {
|
|
608
|
-
console.log(
|
|
609
|
-
chalk.dim(` Try: soulhub search (without query to list all)`)
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
console.log(
|
|
615
|
-
chalk.bold(`
|
|
616
|
-
Found ${agents.length} agent(s):
|
|
617
|
-
`)
|
|
618
|
-
);
|
|
619
|
-
for (const agent of shown) {
|
|
620
|
-
const category = CATEGORY_LABELS[agent.category] || agent.category;
|
|
621
|
-
console.log(
|
|
622
|
-
` ${chalk.cyan.bold(agent.name)} ${chalk.dim(`v${agent.version}`)}`
|
|
623
|
-
);
|
|
624
|
-
console.log(
|
|
625
|
-
` ${agent.displayName} - ${agent.description}`
|
|
626
|
-
);
|
|
627
|
-
console.log(
|
|
628
|
-
` ${chalk.dim(`[${category}]`)} ${chalk.dim(agent.tags.join(", "))}`
|
|
629
|
-
);
|
|
630
|
-
console.log();
|
|
631
|
-
}
|
|
632
|
-
if (agents.length > limit) {
|
|
633
|
-
console.log(
|
|
634
|
-
chalk.dim(
|
|
635
|
-
` ... and ${agents.length - limit} more. Use --limit to show more.`
|
|
636
|
-
)
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
console.log(
|
|
640
|
-
chalk.dim(` Install: soulhub install <name>`)
|
|
641
|
-
);
|
|
642
|
-
console.log();
|
|
643
|
-
} catch (error) {
|
|
644
|
-
logger.errorObj("Search command failed", error);
|
|
645
|
-
console.error(
|
|
646
|
-
chalk.red(`Error: ${error instanceof Error ? error.message : error}`)
|
|
647
|
-
);
|
|
648
|
-
console.error(chalk.dim(` See logs: ${logger.getTodayLogFile()}`));
|
|
649
|
-
process.exit(1);
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
// src/commands/info.ts
|
|
654
|
-
import { Command as Command2 } from "commander";
|
|
655
|
-
import chalk2 from "chalk";
|
|
656
|
-
var infoCommand = new Command2("info").description("View detailed information about an agent template").argument("<name>", "Agent name").option("--identity", "Show IDENTITY.md content").option("--soul", "Show SOUL.md content").action(async (name, options) => {
|
|
657
|
-
try {
|
|
658
|
-
const index = await fetchIndex();
|
|
659
|
-
const agent = index.agents.find((a) => a.name === name);
|
|
660
|
-
if (!agent) {
|
|
661
|
-
console.error(chalk2.red(`Agent "${name}" not found.`));
|
|
662
|
-
console.log(
|
|
663
|
-
chalk2.dim(` Use 'soulhub search' to find available agents.`)
|
|
664
|
-
);
|
|
665
|
-
process.exit(1);
|
|
666
|
-
}
|
|
667
|
-
const category = CATEGORY_LABELS[agent.category] || agent.category;
|
|
668
|
-
console.log();
|
|
669
|
-
console.log(chalk2.bold.cyan(` ${agent.displayName}`));
|
|
670
|
-
console.log(chalk2.dim(` ${agent.name} v${agent.version}`));
|
|
671
|
-
console.log();
|
|
672
|
-
console.log(` ${agent.description}`);
|
|
673
|
-
console.log();
|
|
674
|
-
console.log(
|
|
675
|
-
` ${chalk2.dim("Category:")} ${category}`
|
|
676
|
-
);
|
|
677
|
-
console.log(
|
|
678
|
-
` ${chalk2.dim("Author:")} ${agent.author}`
|
|
679
|
-
);
|
|
680
|
-
console.log(
|
|
681
|
-
` ${chalk2.dim("Tags:")} ${agent.tags.join(", ")}`
|
|
682
|
-
);
|
|
683
|
-
console.log(
|
|
684
|
-
` ${chalk2.dim("Min Claw:")} ${agent.minClawVersion}`
|
|
685
|
-
);
|
|
686
|
-
console.log(
|
|
687
|
-
` ${chalk2.dim("Downloads:")} ${agent.downloads}`
|
|
688
|
-
);
|
|
689
|
-
console.log();
|
|
690
|
-
console.log(chalk2.dim(" Files:"));
|
|
691
|
-
for (const [fileName, size] of Object.entries(agent.files)) {
|
|
692
|
-
const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)} KB` : `${size} B`;
|
|
693
|
-
console.log(` ${fileName} ${chalk2.dim(`(${sizeStr})`)}`);
|
|
694
|
-
}
|
|
695
|
-
if (options.identity) {
|
|
696
|
-
console.log();
|
|
697
|
-
console.log(chalk2.bold(" \u2500\u2500 IDENTITY.md \u2500\u2500"));
|
|
698
|
-
console.log();
|
|
699
|
-
const content = await fetchAgentFile(name, "IDENTITY.md");
|
|
700
|
-
console.log(
|
|
701
|
-
content.split("\n").map((l) => ` ${l}`).join("\n")
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
if (options.soul) {
|
|
705
|
-
console.log();
|
|
706
|
-
console.log(chalk2.bold(" \u2500\u2500 SOUL.md \u2500\u2500"));
|
|
707
|
-
console.log();
|
|
708
|
-
const content = await fetchAgentFile(name, "SOUL.md");
|
|
709
|
-
console.log(
|
|
710
|
-
content.split("\n").map((l) => ` ${l}`).join("\n")
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
console.log();
|
|
714
|
-
console.log(
|
|
715
|
-
chalk2.dim(` Install: soulhub install ${name}`)
|
|
716
|
-
);
|
|
717
|
-
console.log();
|
|
718
|
-
} catch (error) {
|
|
719
|
-
logger.errorObj("Info command failed", error);
|
|
720
|
-
console.error(
|
|
721
|
-
chalk2.red(`Error: ${error instanceof Error ? error.message : error}`)
|
|
722
|
-
);
|
|
723
|
-
console.error(chalk2.dim(` See logs: ${logger.getTodayLogFile()}`));
|
|
724
|
-
process.exit(1);
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// src/commands/install.ts
|
|
729
|
-
import { Command as Command3 } from "commander";
|
|
730
|
-
import chalk3 from "chalk";
|
|
731
|
-
import ora from "ora";
|
|
732
|
-
import fs3 from "fs";
|
|
733
|
-
import path3 from "path";
|
|
734
|
-
import yaml2 from "js-yaml";
|
|
735
|
-
var installCommand = new Command3("install").description("Install an agent or team from the SoulHub registry").argument("[name]", "Agent or team name to install").option("--from <source>", "Install from a local directory, ZIP file, or URL").option(
|
|
736
|
-
"--dir <path>",
|
|
737
|
-
"Target directory (defaults to OpenClaw workspace)"
|
|
738
|
-
).option(
|
|
739
|
-
"--claw-dir <path>",
|
|
740
|
-
"OpenClaw installation directory (overrides OPENCLAW_HOME env var, defaults to ~/.openclaw)"
|
|
741
|
-
).action(async (name, options) => {
|
|
742
|
-
try {
|
|
743
|
-
if (options.from) {
|
|
744
|
-
await installFromSource(options.from, options.dir, options.clawDir);
|
|
745
|
-
} else if (name) {
|
|
746
|
-
await installFromRegistry(name, options.dir, options.clawDir);
|
|
747
|
-
} else {
|
|
748
|
-
console.error(chalk3.red("Please specify an agent or team name, or use --from to install from a local source."));
|
|
749
|
-
console.log(chalk3.dim(" Examples:"));
|
|
750
|
-
console.log(chalk3.dim(" soulhub install writer-wechat # \u4ECE registry \u5B89\u88C5\u5355 agent"));
|
|
751
|
-
console.log(chalk3.dim(" soulhub install dev-squad # \u4ECE registry \u5B89\u88C5\u56E2\u961F"));
|
|
752
|
-
console.log(chalk3.dim(" soulhub install --from ./agent-team/ # \u4ECE\u672C\u5730\u76EE\u5F55\u5B89\u88C5"));
|
|
753
|
-
process.exit(1);
|
|
754
|
-
}
|
|
755
|
-
} catch (error) {
|
|
756
|
-
logger.errorObj("Install command failed", error);
|
|
757
|
-
console.error(
|
|
758
|
-
chalk3.red(`Error: ${error instanceof Error ? error.message : error}`)
|
|
759
|
-
);
|
|
760
|
-
console.error(chalk3.dim(` See logs: ${logger.getTodayLogFile()}`));
|
|
761
|
-
process.exit(1);
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
async function installFromRegistry(name, targetDir, clawDir) {
|
|
765
|
-
const spinner = ora(`Checking registry for ${chalk3.cyan(name)}...`).start();
|
|
766
|
-
const index = await fetchIndex();
|
|
767
|
-
const agent = index.agents.find((a) => a.name === name);
|
|
768
|
-
const recipe = index.recipes.find((r) => r.name === name);
|
|
769
|
-
if (agent && !recipe) {
|
|
770
|
-
spinner.stop();
|
|
771
|
-
logger.info(`Installing single agent from registry: ${name}`);
|
|
772
|
-
await installSingleAgent(name, targetDir, clawDir);
|
|
773
|
-
} else if (recipe) {
|
|
774
|
-
spinner.stop();
|
|
775
|
-
logger.info(`Installing team recipe from registry: ${name}`);
|
|
776
|
-
await installRecipeFromRegistry(name, recipe, targetDir, clawDir);
|
|
777
|
-
} else {
|
|
778
|
-
logger.warn(`"${name}" not found in registry`);
|
|
779
|
-
spinner.fail(`"${name}" not found in registry.`);
|
|
780
|
-
console.log(chalk3.dim(" Use 'soulhub search' to find available agents and teams."));
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
async function installSingleAgent(name, targetDir, clawDir) {
|
|
784
|
-
const spinner = ora(`Checking environment...`).start();
|
|
785
|
-
if (!targetDir) {
|
|
786
|
-
const clawCheck = checkOpenClawInstalled(clawDir);
|
|
787
|
-
if (!clawCheck.installed) {
|
|
788
|
-
spinner.fail("OpenClaw is not installed.");
|
|
789
|
-
printOpenClawInstallHelp();
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
spinner.text = chalk3.dim(`OpenClaw detected: ${clawCheck.clawDir || "via PATH"}`);
|
|
793
|
-
}
|
|
794
|
-
spinner.text = `Fetching agent ${chalk3.cyan(name)}...`;
|
|
795
|
-
const index = await fetchIndex();
|
|
796
|
-
const agent = index.agents.find((a) => a.name === name);
|
|
797
|
-
if (!agent) {
|
|
798
|
-
spinner.fail(`Agent "${name}" not found in registry.`);
|
|
799
|
-
console.log(chalk3.dim(" Use 'soulhub search' to find available agents."));
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
let workspaceDir;
|
|
803
|
-
if (targetDir) {
|
|
804
|
-
workspaceDir = path3.resolve(targetDir);
|
|
805
|
-
} else {
|
|
806
|
-
const resolvedClawDir = findOpenClawDir(clawDir);
|
|
807
|
-
if (!resolvedClawDir) {
|
|
808
|
-
spinner.fail("OpenClaw workspace directory not found.");
|
|
809
|
-
printOpenClawInstallHelp();
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
workspaceDir = getMainWorkspaceDir(resolvedClawDir);
|
|
813
|
-
}
|
|
814
|
-
const resolvedClawDirForBackup = findOpenClawDir(clawDir);
|
|
815
|
-
const backupRecord = !targetDir ? createBackupRecord("single-agent", name, resolvedClawDirForBackup) : null;
|
|
816
|
-
if (!targetDir) {
|
|
817
|
-
const mainCheck = checkMainAgentExists(resolvedClawDirForBackup);
|
|
818
|
-
if (mainCheck.hasContent) {
|
|
819
|
-
spinner.warn(
|
|
820
|
-
`Existing main agent detected. Backing up workspace...`
|
|
821
|
-
);
|
|
822
|
-
const backupDir = backupAgentWorkspace(workspaceDir);
|
|
823
|
-
if (backupDir) {
|
|
824
|
-
console.log(chalk3.yellow(` \u26A0 Existing main agent backed up to: ${backupDir}`));
|
|
825
|
-
addBackupItem(backupRecord, {
|
|
826
|
-
originalPath: workspaceDir,
|
|
827
|
-
backupPath: backupDir,
|
|
828
|
-
method: "cp",
|
|
829
|
-
role: "main",
|
|
830
|
-
agentId: "main"
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
} else {
|
|
835
|
-
const backupDir = backupAgentWorkspace(workspaceDir);
|
|
836
|
-
if (backupDir) {
|
|
837
|
-
console.log(chalk3.yellow(` \u26A0 Existing agent backed up to: ${backupDir}`));
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
if (!fs3.existsSync(workspaceDir)) {
|
|
841
|
-
fs3.mkdirSync(workspaceDir, { recursive: true });
|
|
842
|
-
}
|
|
843
|
-
if (!targetDir) {
|
|
844
|
-
spinner.text = `Registering ${chalk3.cyan(agent.displayName)} as main agent...`;
|
|
845
|
-
const resolvedClawDir = findOpenClawDir(clawDir);
|
|
846
|
-
addAgentToOpenClawConfig(resolvedClawDir, "main", name, true);
|
|
847
|
-
spinner.text = chalk3.dim(`Main agent registered in openclaw.json`);
|
|
848
|
-
}
|
|
849
|
-
spinner.text = `Downloading ${chalk3.cyan(agent.displayName)} soul files...`;
|
|
850
|
-
await downloadAgentFiles(name, workspaceDir, spinner);
|
|
851
|
-
await saveAgentManifest(name, agent, workspaceDir);
|
|
852
|
-
recordInstall(name, agent.version, workspaceDir);
|
|
853
|
-
if (backupRecord) {
|
|
854
|
-
backupRecord.installedMainAgent = name;
|
|
855
|
-
commitBackupRecord(backupRecord);
|
|
856
|
-
}
|
|
857
|
-
logger.info(`Single agent installed: ${name}`, { version: agent.version, workspace: workspaceDir });
|
|
858
|
-
spinner.succeed(
|
|
859
|
-
`${chalk3.cyan.bold(agent.displayName)} installed as main agent!`
|
|
860
|
-
);
|
|
861
|
-
console.log();
|
|
862
|
-
console.log(` ${chalk3.dim("Location:")} ${workspaceDir}`);
|
|
863
|
-
console.log(` ${chalk3.dim("Version:")} ${agent.version}`);
|
|
864
|
-
console.log(` ${chalk3.dim("Type:")} ${chalk3.blue("Single Agent (Main)")}`);
|
|
865
|
-
if (!targetDir) {
|
|
866
|
-
await tryRestartGateway();
|
|
867
|
-
}
|
|
868
|
-
console.log();
|
|
869
|
-
}
|
|
870
|
-
async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
|
|
871
|
-
const spinner = ora(`Installing team ${chalk3.cyan(recipe.displayName)}...`).start();
|
|
872
|
-
if (!targetDir) {
|
|
873
|
-
const clawCheck = checkOpenClawInstalled(clawDir);
|
|
874
|
-
if (!clawCheck.installed) {
|
|
875
|
-
spinner.fail("OpenClaw is not installed.");
|
|
876
|
-
printOpenClawInstallHelp();
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
const resolvedClawDir = targetDir ? path3.resolve(targetDir) : findOpenClawDir(clawDir);
|
|
881
|
-
if (!resolvedClawDir) {
|
|
882
|
-
spinner.fail("OpenClaw workspace directory not found.");
|
|
883
|
-
printOpenClawInstallHelp();
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
spinner.text = `Fetching team configuration...`;
|
|
887
|
-
let pkg;
|
|
888
|
-
try {
|
|
889
|
-
const soulhubYamlContent = await fetchRecipeFile(name, "soulhub.yaml");
|
|
890
|
-
pkg = yaml2.load(soulhubYamlContent);
|
|
891
|
-
} catch {
|
|
892
|
-
spinner.fail(`Failed to fetch soulhub.yaml for recipe "${name}". Recipe packages must include a soulhub.yaml file.`);
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
const recipeBackupRecord = !targetDir ? createBackupRecord("team-registry", name, resolvedClawDir) : null;
|
|
896
|
-
if (!targetDir) {
|
|
897
|
-
spinner.text = "Backing up existing worker agents...";
|
|
898
|
-
const backupResults = backupAllWorkerWorkspaces(resolvedClawDir);
|
|
899
|
-
for (const { name: dirName, backupDir } of backupResults) {
|
|
900
|
-
logger.info(`Existing worker backed up (mv)`, { dirName, backupDir });
|
|
901
|
-
console.log(chalk3.yellow(` \u26A0 Existing ${dirName} moved to: ${backupDir}`));
|
|
902
|
-
const agentId = dirName.replace(/^workspace-/, "");
|
|
903
|
-
addBackupItem(recipeBackupRecord, {
|
|
904
|
-
originalPath: path3.join(resolvedClawDir, dirName),
|
|
905
|
-
backupPath: backupDir,
|
|
906
|
-
method: "mv",
|
|
907
|
-
role: "worker",
|
|
908
|
-
agentId
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
if (backupResults.length > 0) {
|
|
912
|
-
console.log(chalk3.dim(` ${backupResults.length} existing worker(s) backed up.`));
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
if (pkg.dispatcher) {
|
|
916
|
-
spinner.text = `Installing dispatcher ${chalk3.blue(pkg.dispatcher.name)}...`;
|
|
917
|
-
await installDispatcher(pkg.dispatcher, resolvedClawDir, clawDir, targetDir, spinner, recipeBackupRecord);
|
|
918
|
-
}
|
|
919
|
-
const index = await fetchIndex();
|
|
920
|
-
const workerIds = [];
|
|
921
|
-
for (const worker of pkg.agents || []) {
|
|
922
|
-
spinner.text = `Installing worker ${chalk3.cyan(worker.name)}...`;
|
|
923
|
-
const agentName = worker.dir || worker.name;
|
|
924
|
-
const agentId = worker.name;
|
|
925
|
-
const workerDir = targetDir ? path3.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
|
|
926
|
-
if (!targetDir) {
|
|
927
|
-
const regResult = registerAgentToOpenClaw(agentId, workerDir, clawDir);
|
|
928
|
-
if (!regResult.success) {
|
|
929
|
-
console.log(chalk3.yellow(` \u26A0 Failed to register ${agentId}: ${regResult.message}`));
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
} else {
|
|
933
|
-
fs3.mkdirSync(workerDir, { recursive: true });
|
|
934
|
-
}
|
|
935
|
-
await downloadAgentFiles(agentName, workerDir, spinner);
|
|
936
|
-
const agentInfo = index.agents.find((a) => a.name === agentName);
|
|
937
|
-
if (agentInfo) {
|
|
938
|
-
await saveAgentManifest(agentName, agentInfo, workerDir);
|
|
939
|
-
}
|
|
940
|
-
recordInstall(agentId, recipe.version || "1.0.0", workerDir);
|
|
941
|
-
workerIds.push(agentId);
|
|
942
|
-
}
|
|
943
|
-
if (!targetDir) {
|
|
944
|
-
spinner.text = "Configuring multi-agent communication...";
|
|
945
|
-
const dispatcherId = "main";
|
|
946
|
-
configureMultiAgentCommunication(resolvedClawDir, dispatcherId, workerIds);
|
|
947
|
-
}
|
|
948
|
-
if (recipeBackupRecord) {
|
|
949
|
-
recipeBackupRecord.installedWorkerIds = workerIds;
|
|
950
|
-
recipeBackupRecord.installedMainAgent = pkg.dispatcher?.name || null;
|
|
951
|
-
commitBackupRecord(recipeBackupRecord);
|
|
952
|
-
}
|
|
953
|
-
logger.info(`Team installed from registry: ${name}`, { dispatcher: pkg.dispatcher?.name, workers: workerIds });
|
|
954
|
-
spinner.succeed(
|
|
955
|
-
`Team ${chalk3.cyan.bold(recipe.displayName)} installed! (1 dispatcher + ${workerIds.length} workers)`
|
|
956
|
-
);
|
|
957
|
-
printTeamSummary(pkg, workerIds);
|
|
958
|
-
if (!targetDir) {
|
|
959
|
-
await tryRestartGateway();
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
async function installFromSource(source, targetDir, clawDir) {
|
|
963
|
-
const spinner = ora("Analyzing package...").start();
|
|
964
|
-
let packageDir;
|
|
965
|
-
let tempDir = null;
|
|
966
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
967
|
-
logger.info(`Downloading package from URL: ${source}`);
|
|
968
|
-
spinner.text = "Downloading package...";
|
|
969
|
-
const response = await fetch(source, {
|
|
970
|
-
headers: {
|
|
971
|
-
"User-Agent": "soulhub-cli",
|
|
972
|
-
"Accept": "application/zip, application/octet-stream"
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
if (!response.ok) {
|
|
976
|
-
logger.error(`Download failed`, { url: source, status: response.status, statusText: response.statusText });
|
|
977
|
-
spinner.fail(`Failed to download: ${response.statusText}`);
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
const contentType = response.headers.get("content-type") || "";
|
|
981
|
-
logger.debug(`Response content-type: ${contentType}`);
|
|
982
|
-
if (contentType.includes("zip") || source.endsWith(".zip")) {
|
|
983
|
-
const JSZip = (await import("jszip")).default;
|
|
984
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
985
|
-
const zip = await JSZip.loadAsync(arrayBuffer);
|
|
986
|
-
tempDir = path3.join(process.env.HOME || "/tmp", ".soulhub", "tmp", `pkg-${Date.now()}`);
|
|
987
|
-
fs3.mkdirSync(tempDir, { recursive: true });
|
|
988
|
-
await extractZipToDir(zip, tempDir);
|
|
989
|
-
packageDir = tempDir;
|
|
990
|
-
} else {
|
|
991
|
-
logger.error(`Unsupported content type from URL`, { url: source, contentType });
|
|
992
|
-
spinner.fail("Unsupported URL content type. Expected ZIP file.");
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
} else if (source.endsWith(".zip")) {
|
|
996
|
-
spinner.text = "Extracting ZIP file...";
|
|
997
|
-
const JSZip = (await import("jszip")).default;
|
|
998
|
-
const zipData = fs3.readFileSync(path3.resolve(source));
|
|
999
|
-
const zip = await JSZip.loadAsync(zipData);
|
|
1000
|
-
tempDir = path3.join(process.env.HOME || "/tmp", ".soulhub", "tmp", `pkg-${Date.now()}`);
|
|
1001
|
-
fs3.mkdirSync(tempDir, { recursive: true });
|
|
1002
|
-
await extractZipToDir(zip, tempDir);
|
|
1003
|
-
packageDir = tempDir;
|
|
1004
|
-
} else {
|
|
1005
|
-
packageDir = path3.resolve(source);
|
|
1006
|
-
if (!fs3.existsSync(packageDir)) {
|
|
1007
|
-
spinner.fail(`Directory not found: ${packageDir}`);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
const kind = detectPackageKind(packageDir);
|
|
1012
|
-
logger.info(`Detected package type: ${kind}`, { packageDir });
|
|
1013
|
-
spinner.text = `Detected package type: ${chalk3.blue(kind)}`;
|
|
1014
|
-
if (kind === "agent") {
|
|
1015
|
-
spinner.stop();
|
|
1016
|
-
await installSingleAgentFromDir(packageDir, targetDir, clawDir);
|
|
1017
|
-
} else if (kind === "team") {
|
|
1018
|
-
spinner.stop();
|
|
1019
|
-
await installTeamFromDir(packageDir, targetDir, clawDir);
|
|
1020
|
-
} else {
|
|
1021
|
-
logger.error(`Cannot determine package type`, { packageDir, files: fs3.existsSync(packageDir) ? fs3.readdirSync(packageDir) : [] });
|
|
1022
|
-
spinner.fail("Cannot determine package type. Expected soulhub.yaml or IDENTITY.md.");
|
|
1023
|
-
}
|
|
1024
|
-
if (tempDir && fs3.existsSync(tempDir)) {
|
|
1025
|
-
fs3.rmSync(tempDir, { recursive: true, force: true });
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
async function installSingleAgentFromDir(packageDir, targetDir, clawDir) {
|
|
1029
|
-
const spinner = ora("Installing single agent...").start();
|
|
1030
|
-
const pkg = readSoulHubPackage(packageDir);
|
|
1031
|
-
const agentName = pkg?.name || path3.basename(packageDir);
|
|
1032
|
-
if (!targetDir) {
|
|
1033
|
-
const clawCheck = checkOpenClawInstalled(clawDir);
|
|
1034
|
-
if (!clawCheck.installed) {
|
|
1035
|
-
spinner.fail("OpenClaw is not installed.");
|
|
1036
|
-
printOpenClawInstallHelp();
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
let workspaceDir;
|
|
1041
|
-
if (targetDir) {
|
|
1042
|
-
workspaceDir = path3.resolve(targetDir);
|
|
1043
|
-
} else {
|
|
1044
|
-
const resolvedClawDir = findOpenClawDir(clawDir);
|
|
1045
|
-
if (!resolvedClawDir) {
|
|
1046
|
-
spinner.fail("OpenClaw workspace directory not found.");
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
workspaceDir = getMainWorkspaceDir(resolvedClawDir);
|
|
1050
|
-
}
|
|
1051
|
-
const localBackupRecord = !targetDir ? createBackupRecord("single-agent-local", agentName, findOpenClawDir(clawDir)) : null;
|
|
1052
|
-
if (!targetDir) {
|
|
1053
|
-
const resolvedClawDir = findOpenClawDir(clawDir);
|
|
1054
|
-
const mainCheck = checkMainAgentExists(resolvedClawDir);
|
|
1055
|
-
if (mainCheck.hasContent) {
|
|
1056
|
-
spinner.warn("Existing main agent detected. Backing up...");
|
|
1057
|
-
const backupDir = backupAgentWorkspace(workspaceDir);
|
|
1058
|
-
if (backupDir) {
|
|
1059
|
-
console.log(chalk3.yellow(` \u26A0 Existing main agent backed up to: ${backupDir}`));
|
|
1060
|
-
addBackupItem(localBackupRecord, {
|
|
1061
|
-
originalPath: workspaceDir,
|
|
1062
|
-
backupPath: backupDir,
|
|
1063
|
-
method: "cp",
|
|
1064
|
-
role: "main",
|
|
1065
|
-
agentId: "main"
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
if (!fs3.existsSync(workspaceDir)) {
|
|
1071
|
-
fs3.mkdirSync(workspaceDir, { recursive: true });
|
|
1072
|
-
}
|
|
1073
|
-
if (!targetDir) {
|
|
1074
|
-
spinner.text = `Registering ${chalk3.cyan(agentName)} as main agent...`;
|
|
1075
|
-
const resolvedClawDir = findOpenClawDir(clawDir);
|
|
1076
|
-
addAgentToOpenClawConfig(resolvedClawDir, "main", agentName, true);
|
|
1077
|
-
}
|
|
1078
|
-
spinner.text = `Copying soul files...`;
|
|
1079
|
-
copyAgentFilesFromDir(packageDir, workspaceDir);
|
|
1080
|
-
recordInstall(agentName, pkg?.version || "local", workspaceDir);
|
|
1081
|
-
if (localBackupRecord) {
|
|
1082
|
-
localBackupRecord.installedMainAgent = agentName;
|
|
1083
|
-
commitBackupRecord(localBackupRecord);
|
|
1084
|
-
}
|
|
1085
|
-
logger.info(`Single agent installed from dir: ${agentName}`, { source: packageDir, workspace: workspaceDir });
|
|
1086
|
-
spinner.succeed(`${chalk3.cyan.bold(agentName)} installed as main agent!`);
|
|
1087
|
-
console.log();
|
|
1088
|
-
console.log(` ${chalk3.dim("Location:")} ${workspaceDir}`);
|
|
1089
|
-
console.log(` ${chalk3.dim("Source:")} ${packageDir}`);
|
|
1090
|
-
console.log(` ${chalk3.dim("Type:")} ${chalk3.blue("Single Agent (Main)")}`);
|
|
1091
|
-
if (!targetDir) {
|
|
1092
|
-
await tryRestartGateway();
|
|
1093
|
-
}
|
|
1094
|
-
console.log();
|
|
1095
|
-
}
|
|
1096
|
-
async function installTeamFromDir(packageDir, targetDir, clawDir) {
|
|
1097
|
-
const spinner = ora("Installing team...").start();
|
|
1098
|
-
const pkg = readSoulHubPackage(packageDir);
|
|
1099
|
-
if (!pkg || pkg.kind !== "team") {
|
|
1100
|
-
spinner.fail("Invalid team package. Missing soulhub.yaml.");
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
if (!targetDir) {
|
|
1104
|
-
const clawCheck = checkOpenClawInstalled(clawDir);
|
|
1105
|
-
if (!clawCheck.installed) {
|
|
1106
|
-
spinner.fail("OpenClaw is not installed.");
|
|
1107
|
-
printOpenClawInstallHelp();
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
const resolvedClawDir = targetDir ? path3.resolve(targetDir) : findOpenClawDir(clawDir);
|
|
1112
|
-
if (!resolvedClawDir) {
|
|
1113
|
-
spinner.fail("OpenClaw workspace directory not found.");
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
const teamBackupRecord = !targetDir ? createBackupRecord("team-local", pkg.name, resolvedClawDir) : null;
|
|
1117
|
-
if (!targetDir) {
|
|
1118
|
-
spinner.text = "Backing up existing worker agents...";
|
|
1119
|
-
const backupResults = backupAllWorkerWorkspaces(resolvedClawDir);
|
|
1120
|
-
for (const { name: dirName, backupDir } of backupResults) {
|
|
1121
|
-
logger.info(`Existing worker backed up (mv)`, { dirName, backupDir });
|
|
1122
|
-
console.log(chalk3.yellow(` \u26A0 Existing ${dirName} moved to: ${backupDir}`));
|
|
1123
|
-
const agentId = dirName.replace(/^workspace-/, "");
|
|
1124
|
-
addBackupItem(teamBackupRecord, {
|
|
1125
|
-
originalPath: path3.join(resolvedClawDir, dirName),
|
|
1126
|
-
backupPath: backupDir,
|
|
1127
|
-
method: "mv",
|
|
1128
|
-
role: "worker",
|
|
1129
|
-
agentId
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
if (backupResults.length > 0) {
|
|
1133
|
-
console.log(chalk3.dim(` ${backupResults.length} existing worker(s) backed up.`));
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
if (pkg.dispatcher) {
|
|
1137
|
-
spinner.text = `Installing dispatcher ${chalk3.blue(pkg.dispatcher.name)}...`;
|
|
1138
|
-
const mainWorkspace = targetDir ? path3.join(resolvedClawDir, "workspace") : getMainWorkspaceDir(resolvedClawDir);
|
|
1139
|
-
if (!targetDir) {
|
|
1140
|
-
const mainCheck = checkMainAgentExists(resolvedClawDir);
|
|
1141
|
-
if (mainCheck.hasContent) {
|
|
1142
|
-
spinner.warn("Existing main agent detected. Backing up...");
|
|
1143
|
-
const backupDir = backupAgentWorkspace(mainWorkspace);
|
|
1144
|
-
if (backupDir) {
|
|
1145
|
-
console.log(chalk3.yellow(` \u26A0 Existing main agent backed up to: ${backupDir}`));
|
|
1146
|
-
if (teamBackupRecord) {
|
|
1147
|
-
addBackupItem(teamBackupRecord, {
|
|
1148
|
-
originalPath: mainWorkspace,
|
|
1149
|
-
backupPath: backupDir,
|
|
1150
|
-
method: "cp",
|
|
1151
|
-
role: "main",
|
|
1152
|
-
agentId: "main"
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
if (!fs3.existsSync(mainWorkspace)) {
|
|
1159
|
-
fs3.mkdirSync(mainWorkspace, { recursive: true });
|
|
1160
|
-
}
|
|
1161
|
-
if (!targetDir) {
|
|
1162
|
-
addAgentToOpenClawConfig(resolvedClawDir, "main", pkg.dispatcher.name, true);
|
|
1163
|
-
}
|
|
1164
|
-
const dispatcherSourceDir = path3.join(packageDir, pkg.dispatcher.dir);
|
|
1165
|
-
if (fs3.existsSync(dispatcherSourceDir)) {
|
|
1166
|
-
copyAgentFilesFromDir(dispatcherSourceDir, mainWorkspace);
|
|
1167
|
-
}
|
|
1168
|
-
recordInstall("dispatcher", pkg.version || "local", mainWorkspace);
|
|
1169
|
-
}
|
|
1170
|
-
const workerIds = [];
|
|
1171
|
-
for (const worker of pkg.agents || []) {
|
|
1172
|
-
const agentId = worker.name;
|
|
1173
|
-
const agentDir = worker.dir || worker.name;
|
|
1174
|
-
spinner.text = `Installing worker ${chalk3.cyan(agentId)}...`;
|
|
1175
|
-
const workerWorkspace = targetDir ? path3.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
|
|
1176
|
-
if (!targetDir) {
|
|
1177
|
-
const regResult = registerAgentToOpenClaw(agentId, workerWorkspace, clawDir);
|
|
1178
|
-
if (!regResult.success) {
|
|
1179
|
-
console.log(chalk3.yellow(` \u26A0 Failed to register ${agentId}: ${regResult.message}`));
|
|
1180
|
-
continue;
|
|
1181
|
-
}
|
|
1182
|
-
} else {
|
|
1183
|
-
fs3.mkdirSync(workerWorkspace, { recursive: true });
|
|
1184
|
-
}
|
|
1185
|
-
const workerSourceDir = path3.join(packageDir, agentDir);
|
|
1186
|
-
if (fs3.existsSync(workerSourceDir)) {
|
|
1187
|
-
copyAgentFilesFromDir(workerSourceDir, workerWorkspace);
|
|
1188
|
-
}
|
|
1189
|
-
recordInstall(agentId, pkg.version || "local", workerWorkspace);
|
|
1190
|
-
workerIds.push(agentId);
|
|
1191
|
-
}
|
|
1192
|
-
if (!targetDir && workerIds.length > 0) {
|
|
1193
|
-
spinner.text = "Configuring multi-agent communication...";
|
|
1194
|
-
configureMultiAgentCommunication(resolvedClawDir, "main", workerIds);
|
|
1195
|
-
}
|
|
1196
|
-
if (teamBackupRecord) {
|
|
1197
|
-
teamBackupRecord.installedWorkerIds = workerIds;
|
|
1198
|
-
teamBackupRecord.installedMainAgent = pkg.dispatcher?.name || null;
|
|
1199
|
-
commitBackupRecord(teamBackupRecord);
|
|
1200
|
-
}
|
|
1201
|
-
logger.info(`Team installed from dir: ${pkg.name}`, { dispatcher: pkg.dispatcher?.name, workers: workerIds, source: packageDir });
|
|
1202
|
-
spinner.succeed(
|
|
1203
|
-
`Team ${chalk3.cyan.bold(pkg.name)} installed! (${pkg.dispatcher ? "1 dispatcher + " : ""}${workerIds.length} workers)`
|
|
1204
|
-
);
|
|
1205
|
-
printTeamSummary(pkg, workerIds);
|
|
1206
|
-
if (!targetDir) {
|
|
1207
|
-
await tryRestartGateway();
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
async function downloadAgentFiles(agentName, workspaceDir, spinner) {
|
|
1211
|
-
fs3.mkdirSync(workspaceDir, { recursive: true });
|
|
1212
|
-
const coreFiles = ["IDENTITY.md", "SOUL.md"];
|
|
1213
|
-
for (const fileName of coreFiles) {
|
|
1214
|
-
const content = await fetchAgentFile(agentName, fileName);
|
|
1215
|
-
fs3.writeFileSync(path3.join(workspaceDir, fileName), content);
|
|
1216
|
-
spinner.text = `Downloaded ${fileName}`;
|
|
1217
|
-
}
|
|
1218
|
-
const optionalFiles = ["USER.md.template", "TOOLS.md.template"];
|
|
1219
|
-
for (const fileName of optionalFiles) {
|
|
1220
|
-
try {
|
|
1221
|
-
const content = await fetchAgentFile(agentName, fileName);
|
|
1222
|
-
const actualName = fileName.replace(".template", "");
|
|
1223
|
-
fs3.writeFileSync(path3.join(workspaceDir, actualName), content);
|
|
1224
|
-
} catch {
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
async function saveAgentManifest(agentName, agent, workspaceDir) {
|
|
1229
|
-
try {
|
|
1230
|
-
const manifestContent = await fetchAgentFile(agentName, "manifest.yaml");
|
|
1231
|
-
fs3.writeFileSync(path3.join(workspaceDir, "manifest.yaml"), manifestContent);
|
|
1232
|
-
} catch {
|
|
1233
|
-
const manifest = {
|
|
1234
|
-
name: agent.name,
|
|
1235
|
-
displayName: agent.displayName,
|
|
1236
|
-
description: agent.description,
|
|
1237
|
-
category: agent.category,
|
|
1238
|
-
tags: agent.tags,
|
|
1239
|
-
version: agent.version,
|
|
1240
|
-
author: agent.author
|
|
1241
|
-
};
|
|
1242
|
-
fs3.writeFileSync(path3.join(workspaceDir, "manifest.yaml"), yaml2.dump(manifest));
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
function copyAgentFilesFromDir(sourceDir, targetDir) {
|
|
1246
|
-
const filesToCopy = ["IDENTITY.md", "SOUL.md", "USER.md", "TOOLS.md", "AGENTS.md", "HEARTBEAT.md"];
|
|
1247
|
-
for (const fileName of filesToCopy) {
|
|
1248
|
-
const sourcePath = path3.join(sourceDir, fileName);
|
|
1249
|
-
if (fs3.existsSync(sourcePath)) {
|
|
1250
|
-
fs3.mkdirSync(targetDir, { recursive: true });
|
|
1251
|
-
fs3.copyFileSync(sourcePath, path3.join(targetDir, fileName));
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
async function installDispatcher(dispatcher, resolvedClawDir, clawDir, targetDir, spinner, backupRecord) {
|
|
1256
|
-
const mainWorkspace = targetDir ? path3.join(resolvedClawDir, "workspace") : getMainWorkspaceDir(resolvedClawDir);
|
|
1257
|
-
if (!targetDir) {
|
|
1258
|
-
const mainCheck = checkMainAgentExists(resolvedClawDir);
|
|
1259
|
-
if (mainCheck.hasContent) {
|
|
1260
|
-
if (spinner) spinner.warn("Existing main agent detected. Backing up...");
|
|
1261
|
-
const backupDir = backupAgentWorkspace(mainWorkspace);
|
|
1262
|
-
if (backupDir) {
|
|
1263
|
-
console.log(chalk3.yellow(` \u26A0 Existing main agent backed up to: ${backupDir}`));
|
|
1264
|
-
if (backupRecord) {
|
|
1265
|
-
addBackupItem(backupRecord, {
|
|
1266
|
-
originalPath: mainWorkspace,
|
|
1267
|
-
backupPath: backupDir,
|
|
1268
|
-
method: "cp",
|
|
1269
|
-
role: "main",
|
|
1270
|
-
agentId: "main"
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
if (!fs3.existsSync(mainWorkspace)) {
|
|
1277
|
-
fs3.mkdirSync(mainWorkspace, { recursive: true });
|
|
1278
|
-
}
|
|
1279
|
-
if (!targetDir) {
|
|
1280
|
-
addAgentToOpenClawConfig(resolvedClawDir, "main", dispatcher.name, true);
|
|
1281
|
-
}
|
|
1282
|
-
const templateName = dispatcher.dir || dispatcher.name;
|
|
1283
|
-
if (spinner) spinner.text = `Downloading dispatcher files...`;
|
|
1284
|
-
await downloadAgentFiles(templateName, mainWorkspace, spinner || ora());
|
|
1285
|
-
recordInstall("dispatcher", "1.0.0", mainWorkspace);
|
|
1286
|
-
}
|
|
1287
|
-
async function extractZipToDir(zip, targetDir) {
|
|
1288
|
-
const entries = Object.entries(zip.files);
|
|
1289
|
-
for (const [relativePath, file] of entries) {
|
|
1290
|
-
const fullPath = path3.join(targetDir, relativePath);
|
|
1291
|
-
if (file.dir) {
|
|
1292
|
-
fs3.mkdirSync(fullPath, { recursive: true });
|
|
1293
|
-
} else {
|
|
1294
|
-
fs3.mkdirSync(path3.dirname(fullPath), { recursive: true });
|
|
1295
|
-
const content = await file.async("nodebuffer");
|
|
1296
|
-
fs3.writeFileSync(fullPath, content);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
function printOpenClawInstallHelp() {
|
|
1301
|
-
console.log(chalk3.dim(" Please install OpenClaw first, or use one of the following options:"));
|
|
1302
|
-
console.log(chalk3.dim(" --claw-dir <path> Specify OpenClaw installation directory"));
|
|
1303
|
-
console.log(chalk3.dim(" --dir <path> Specify agent target directory directly"));
|
|
1304
|
-
console.log(chalk3.dim(" OPENCLAW_HOME=<path> Set environment variable"));
|
|
1305
|
-
console.log(chalk3.dim(" Visit: https://github.com/anthropics/openclaw for installation instructions."));
|
|
1306
|
-
}
|
|
1307
|
-
function printTeamSummary(pkg, workerIds) {
|
|
1308
|
-
console.log();
|
|
1309
|
-
if (pkg.dispatcher) {
|
|
1310
|
-
console.log(` ${chalk3.blue("\u26A1 [Dispatcher]")} ${chalk3.cyan(pkg.dispatcher.name)} \u2192 ${chalk3.dim("workspace/")}`);
|
|
1311
|
-
}
|
|
1312
|
-
for (const id of workerIds) {
|
|
1313
|
-
console.log(` ${chalk3.dim(" \u2713 [Worker]")} ${chalk3.cyan(id)} \u2192 ${chalk3.dim(`workspace-${id}/`)}`);
|
|
1314
|
-
}
|
|
1315
|
-
console.log();
|
|
1316
|
-
console.log(` ${chalk3.dim("Type:")} ${chalk3.blue("Multi-Agent Team")}`);
|
|
1317
|
-
if (pkg.routing && pkg.routing.length > 0) {
|
|
1318
|
-
console.log(` ${chalk3.dim("Routing:")} ${pkg.routing.length} rules configured`);
|
|
1319
|
-
}
|
|
1320
|
-
console.log();
|
|
1321
|
-
}
|
|
1322
|
-
async function tryRestartGateway() {
|
|
1323
|
-
const restartSpinner = ora("Restarting OpenClaw Gateway...").start();
|
|
1324
|
-
const result = restartOpenClawGateway();
|
|
1325
|
-
if (result.success) {
|
|
1326
|
-
restartSpinner.succeed("OpenClaw Gateway restarted successfully.");
|
|
1327
|
-
} else {
|
|
1328
|
-
restartSpinner.warn("Failed to restart OpenClaw Gateway.");
|
|
1329
|
-
console.log(chalk3.yellow(` Reason: ${result.message}`));
|
|
1330
|
-
console.log(chalk3.dim(" Please restart manually:"));
|
|
1331
|
-
console.log(chalk3.dim(" openclaw gateway restart"));
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// src/commands/list.ts
|
|
1336
|
-
import { Command as Command4 } from "commander";
|
|
1337
|
-
import chalk4 from "chalk";
|
|
1338
|
-
var listCommand = new Command4("list").description("List installed agent templates").alias("ls").action(async () => {
|
|
1339
|
-
try {
|
|
1340
|
-
const config = loadConfig();
|
|
1341
|
-
if (config.installed.length === 0) {
|
|
1342
|
-
console.log(chalk4.yellow("\n No agents installed yet.\n"));
|
|
1343
|
-
console.log(
|
|
1344
|
-
chalk4.dim(" Install one: soulhub install <name>")
|
|
1345
|
-
);
|
|
1346
|
-
console.log(
|
|
1347
|
-
chalk4.dim(" Browse all: soulhub search\n")
|
|
1348
|
-
);
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
console.log(
|
|
1352
|
-
chalk4.bold(`
|
|
1353
|
-
Installed agents (${config.installed.length}):
|
|
1354
|
-
`)
|
|
1355
|
-
);
|
|
1356
|
-
for (const agent of config.installed) {
|
|
1357
|
-
const date = new Date(agent.installedAt).toLocaleDateString();
|
|
1358
|
-
console.log(
|
|
1359
|
-
` ${chalk4.cyan.bold(agent.name)} ${chalk4.dim(`v${agent.version}`)}`
|
|
1360
|
-
);
|
|
1361
|
-
console.log(
|
|
1362
|
-
` ${chalk4.dim("Installed:")} ${date} ${chalk4.dim("Location:")} ${agent.workspace}`
|
|
1363
|
-
);
|
|
1364
|
-
console.log();
|
|
1365
|
-
}
|
|
1366
|
-
} catch (error) {
|
|
1367
|
-
logger.errorObj("List command failed", error);
|
|
1368
|
-
console.error(
|
|
1369
|
-
chalk4.red(`Error: ${error instanceof Error ? error.message : error}`)
|
|
1370
|
-
);
|
|
1371
|
-
console.error(chalk4.dim(` See logs: ${logger.getTodayLogFile()}`));
|
|
1372
|
-
process.exit(1);
|
|
1373
|
-
}
|
|
1374
|
-
});
|
|
1375
|
-
|
|
1376
|
-
// src/commands/update.ts
|
|
1377
|
-
import { Command as Command5 } from "commander";
|
|
1378
|
-
import chalk5 from "chalk";
|
|
1379
|
-
import ora2 from "ora";
|
|
1380
|
-
import fs4 from "fs";
|
|
1381
|
-
import path4 from "path";
|
|
1382
|
-
var updateCommand = new Command5("update").description("Update installed agent templates to latest versions").argument("[name]", "Agent name to update (updates all if omitted)").action(async (name) => {
|
|
1383
|
-
try {
|
|
1384
|
-
const config = loadConfig();
|
|
1385
|
-
if (config.installed.length === 0) {
|
|
1386
|
-
console.log(chalk5.yellow("\n No agents installed.\n"));
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
const spinner = ora2("Checking for updates...").start();
|
|
1390
|
-
const index = await fetchIndex();
|
|
1391
|
-
const toUpdate = name ? config.installed.filter((a) => a.name === name) : config.installed;
|
|
1392
|
-
if (toUpdate.length === 0) {
|
|
1393
|
-
spinner.fail(`Agent "${name}" is not installed.`);
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
let updated = 0;
|
|
1397
|
-
for (const installed of toUpdate) {
|
|
1398
|
-
const remote = index.agents.find(
|
|
1399
|
-
(a) => a.name === installed.name
|
|
1400
|
-
);
|
|
1401
|
-
if (!remote) {
|
|
1402
|
-
spinner.text = `${installed.name}: not found in registry, skipping`;
|
|
1403
|
-
continue;
|
|
1404
|
-
}
|
|
1405
|
-
if (remote.version === installed.version) {
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
spinner.text = `Updating ${chalk5.cyan(installed.name)} (${installed.version} \u2192 ${remote.version})...`;
|
|
1409
|
-
const workspaceDir = installed.workspace;
|
|
1410
|
-
if (!fs4.existsSync(workspaceDir)) {
|
|
1411
|
-
fs4.mkdirSync(workspaceDir, { recursive: true });
|
|
1412
|
-
}
|
|
1413
|
-
for (const fileName of ["IDENTITY.md", "SOUL.md", "manifest.yaml"]) {
|
|
1414
|
-
try {
|
|
1415
|
-
const content = await fetchAgentFile(
|
|
1416
|
-
installed.name,
|
|
1417
|
-
fileName
|
|
1418
|
-
);
|
|
1419
|
-
fs4.writeFileSync(
|
|
1420
|
-
path4.join(workspaceDir, fileName),
|
|
1421
|
-
content
|
|
1422
|
-
);
|
|
1423
|
-
} catch {
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
installed.version = remote.version;
|
|
1427
|
-
installed.installedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1428
|
-
updated++;
|
|
1429
|
-
}
|
|
1430
|
-
saveConfig(config);
|
|
1431
|
-
if (updated === 0) {
|
|
1432
|
-
spinner.succeed("All agents are up to date.");
|
|
1433
|
-
} else {
|
|
1434
|
-
logger.info(`Updated ${updated} agent(s)`);
|
|
1435
|
-
spinner.succeed(`Updated ${updated} agent(s).`);
|
|
1436
|
-
}
|
|
1437
|
-
console.log();
|
|
1438
|
-
} catch (error) {
|
|
1439
|
-
logger.errorObj("Update command failed", error);
|
|
1440
|
-
console.error(
|
|
1441
|
-
chalk5.red(`Error: ${error instanceof Error ? error.message : error}`)
|
|
1442
|
-
);
|
|
1443
|
-
console.error(chalk5.dim(` See logs: ${logger.getTodayLogFile()}`));
|
|
1444
|
-
process.exit(1);
|
|
1445
|
-
}
|
|
1446
|
-
});
|
|
1447
|
-
|
|
1448
|
-
// src/commands/uninstall.ts
|
|
1449
|
-
import { Command as Command6 } from "commander";
|
|
1450
|
-
import chalk6 from "chalk";
|
|
1451
|
-
import ora3 from "ora";
|
|
1452
|
-
import fs5 from "fs";
|
|
1453
|
-
var uninstallCommand = new Command6("uninstall").description("Uninstall an agent template").alias("rm").argument("<name>", "Agent name to uninstall").option("--keep-files", "Remove from registry but keep workspace files").action(async (name, options) => {
|
|
1454
|
-
try {
|
|
1455
|
-
const config = loadConfig();
|
|
1456
|
-
const installed = config.installed.find((a) => a.name === name);
|
|
1457
|
-
if (!installed) {
|
|
1458
|
-
console.error(
|
|
1459
|
-
chalk6.red(`
|
|
1460
|
-
Agent "${name}" is not installed.
|
|
1461
|
-
`)
|
|
1462
|
-
);
|
|
1463
|
-
console.log(
|
|
1464
|
-
chalk6.dim(" Use 'soulhub list' to see installed agents.")
|
|
1465
|
-
);
|
|
1466
|
-
process.exit(1);
|
|
1467
|
-
}
|
|
1468
|
-
const spinner = ora3(
|
|
1469
|
-
`Uninstalling ${chalk6.cyan(name)}...`
|
|
1470
|
-
).start();
|
|
1471
|
-
if (!options.keepFiles && fs5.existsSync(installed.workspace)) {
|
|
1472
|
-
fs5.rmSync(installed.workspace, { recursive: true, force: true });
|
|
1473
|
-
spinner.text = `Removed workspace: ${installed.workspace}`;
|
|
1474
|
-
}
|
|
1475
|
-
removeInstallRecord(name);
|
|
1476
|
-
logger.info(`Agent uninstalled: ${name}`, { workspace: installed.workspace, keepFiles: !!options.keepFiles });
|
|
1477
|
-
spinner.succeed(
|
|
1478
|
-
`${chalk6.cyan.bold(name)} uninstalled.`
|
|
1479
|
-
);
|
|
1480
|
-
if (options.keepFiles) {
|
|
1481
|
-
console.log(
|
|
1482
|
-
chalk6.dim(` Files kept at: ${installed.workspace}`)
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
console.log();
|
|
1486
|
-
} catch (error) {
|
|
1487
|
-
logger.errorObj("Uninstall command failed", error);
|
|
1488
|
-
console.error(
|
|
1489
|
-
chalk6.red(`Error: ${error instanceof Error ? error.message : error}`)
|
|
1490
|
-
);
|
|
1491
|
-
console.error(chalk6.dim(` See logs: ${logger.getTodayLogFile()}`));
|
|
1492
|
-
process.exit(1);
|
|
1493
|
-
}
|
|
1494
|
-
});
|
|
1495
|
-
|
|
1496
|
-
// src/commands/publish.ts
|
|
1497
|
-
import { Command as Command7 } from "commander";
|
|
1498
|
-
import chalk7 from "chalk";
|
|
1499
|
-
import ora4 from "ora";
|
|
1500
|
-
import fs6 from "fs";
|
|
1501
|
-
import path5 from "path";
|
|
1502
|
-
import yaml3 from "js-yaml";
|
|
1503
|
-
var publishCommand = new Command7("publish").description("Publish an agent template to the SoulHub community").argument(
|
|
1504
|
-
"[directory]",
|
|
1505
|
-
"Agent workspace directory (defaults to current directory)"
|
|
1506
|
-
).action(async (directory) => {
|
|
1507
|
-
try {
|
|
1508
|
-
const dir = directory ? path5.resolve(directory) : process.cwd();
|
|
1509
|
-
const spinner = ora4("Validating agent template...").start();
|
|
1510
|
-
const requiredFiles = ["manifest.yaml", "IDENTITY.md", "SOUL.md"];
|
|
1511
|
-
const missing = requiredFiles.filter(
|
|
1512
|
-
(f) => !fs6.existsSync(path5.join(dir, f))
|
|
1513
|
-
);
|
|
1514
|
-
if (missing.length > 0) {
|
|
1515
|
-
spinner.fail("Missing required files:");
|
|
1516
|
-
for (const f of missing) {
|
|
1517
|
-
console.log(chalk7.red(` - ${f}`));
|
|
1518
|
-
}
|
|
1519
|
-
console.log();
|
|
1520
|
-
console.log(chalk7.dim(" Required files: manifest.yaml, IDENTITY.md, SOUL.md"));
|
|
1521
|
-
console.log(
|
|
1522
|
-
chalk7.dim(
|
|
1523
|
-
" See: https://soulhub.dev/docs/contributing"
|
|
1524
|
-
)
|
|
1525
|
-
);
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
const manifestContent = fs6.readFileSync(
|
|
1529
|
-
path5.join(dir, "manifest.yaml"),
|
|
1530
|
-
"utf-8"
|
|
1531
|
-
);
|
|
1532
|
-
const manifest = yaml3.load(manifestContent);
|
|
1533
|
-
const requiredFields = [
|
|
1534
|
-
"name",
|
|
1535
|
-
"displayName",
|
|
1536
|
-
"description",
|
|
1537
|
-
"category",
|
|
1538
|
-
"version",
|
|
1539
|
-
"author"
|
|
1540
|
-
];
|
|
1541
|
-
const missingFields = requiredFields.filter(
|
|
1542
|
-
(f) => !manifest[f]
|
|
1543
|
-
);
|
|
1544
|
-
if (missingFields.length > 0) {
|
|
1545
|
-
spinner.fail("Missing required fields in manifest.yaml:");
|
|
1546
|
-
for (const f of missingFields) {
|
|
1547
|
-
console.log(chalk7.red(` - ${f}`));
|
|
1548
|
-
}
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
const validCategories = [
|
|
1552
|
-
"self-media",
|
|
1553
|
-
"development",
|
|
1554
|
-
"operations",
|
|
1555
|
-
"support",
|
|
1556
|
-
"education",
|
|
1557
|
-
"dispatcher"
|
|
1558
|
-
];
|
|
1559
|
-
if (!validCategories.includes(manifest.category)) {
|
|
1560
|
-
spinner.fail(
|
|
1561
|
-
`Invalid category: ${manifest.category}. Must be one of: ${validCategories.join(", ")}`
|
|
1562
|
-
);
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
spinner.succeed("Template validation passed!");
|
|
1566
|
-
console.log();
|
|
1567
|
-
console.log(chalk7.bold(` ${manifest.displayName}`));
|
|
1568
|
-
console.log(
|
|
1569
|
-
chalk7.dim(` ${manifest.name} v${manifest.version}`)
|
|
1570
|
-
);
|
|
1571
|
-
console.log(` ${manifest.description}`);
|
|
1572
|
-
console.log();
|
|
1573
|
-
console.log(chalk7.bold(" Next steps to publish:"));
|
|
1574
|
-
console.log();
|
|
1575
|
-
console.log(
|
|
1576
|
-
` 1. Fork ${chalk7.cyan("github.com/soulhub-community/soulhub")}`
|
|
1577
|
-
);
|
|
1578
|
-
console.log(
|
|
1579
|
-
` 2. Copy your agent directory to ${chalk7.cyan(`registry/agents/${manifest.name}/`)}`
|
|
1580
|
-
);
|
|
1581
|
-
console.log(
|
|
1582
|
-
` 3. Submit a Pull Request`
|
|
1583
|
-
);
|
|
1584
|
-
console.log();
|
|
1585
|
-
console.log(
|
|
1586
|
-
chalk7.dim(
|
|
1587
|
-
" Your template will be reviewed and added to the community registry."
|
|
1588
|
-
)
|
|
1589
|
-
);
|
|
1590
|
-
console.log();
|
|
1591
|
-
} catch (error) {
|
|
1592
|
-
logger.errorObj("Publish command failed", error);
|
|
1593
|
-
console.error(
|
|
1594
|
-
chalk7.red(`Error: ${error instanceof Error ? error.message : error}`)
|
|
1595
|
-
);
|
|
1596
|
-
console.error(chalk7.dim(` See logs: ${logger.getTodayLogFile()}`));
|
|
1597
|
-
process.exit(1);
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
|
|
1601
|
-
// src/index.ts
|
|
1602
|
-
var program = new Command8();
|
|
1603
|
-
program.name("soulhub").description("SoulHub CLI - Install and manage AI agent persona templates").version("0.1.0").option("--verbose", "Enable verbose debug logging").hook("preAction", () => {
|
|
1604
|
-
const opts = program.opts();
|
|
1605
|
-
const verbose = opts.verbose || process.env.SOULHUB_DEBUG === "1";
|
|
1606
|
-
logger.init(verbose);
|
|
1607
|
-
logger.info("CLI started", {
|
|
1608
|
-
args: process.argv.slice(2),
|
|
1609
|
-
version: "0.1.0",
|
|
1610
|
-
node: process.version
|
|
1611
|
-
});
|
|
1612
|
-
});
|
|
1613
|
-
program.addCommand(searchCommand);
|
|
1614
|
-
program.addCommand(infoCommand);
|
|
1615
|
-
program.addCommand(installCommand);
|
|
1616
|
-
program.addCommand(listCommand);
|
|
1617
|
-
program.addCommand(updateCommand);
|
|
1618
|
-
program.addCommand(uninstallCommand);
|
|
1619
|
-
program.addCommand(publishCommand);
|
|
1620
|
-
program.parse();
|