soulhubcli 1.0.4 → 1.0.6

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.
Files changed (2) hide show
  1. package/dist/index.js +492 -117
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8,26 +8,158 @@ import { Command } from "commander";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/utils.ts
11
- import fs from "fs";
12
- import path from "path";
11
+ import fs2 from "fs";
12
+ import path2 from "path";
13
13
  import { execSync } from "child_process";
14
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
15
143
  var DEFAULT_REGISTRY_URL = "http://soulhub.store";
16
144
  function getRegistryUrl() {
17
145
  return process.env.SOULHUB_REGISTRY_URL || DEFAULT_REGISTRY_URL;
18
146
  }
19
147
  async function fetchIndex() {
20
148
  const url = `${getRegistryUrl()}/index.json`;
149
+ logger.debug(`Fetching registry index`, { url });
21
150
  const response = await fetch(url);
22
151
  if (!response.ok) {
152
+ logger.error(`Failed to fetch registry index`, { url, status: response.status, statusText: response.statusText });
23
153
  throw new Error(`Failed to fetch registry index: ${response.statusText}`);
24
154
  }
25
155
  return await response.json();
26
156
  }
27
157
  async function fetchAgentFile(agentName, fileName) {
28
158
  const url = `${getRegistryUrl()}/agents/${agentName}/${fileName}`;
159
+ logger.debug(`Fetching agent file`, { agentName, fileName, url });
29
160
  const response = await fetch(url);
30
161
  if (!response.ok) {
162
+ logger.error(`Failed to fetch agent file`, { agentName, fileName, url, status: response.status });
31
163
  throw new Error(
32
164
  `Failed to fetch ${fileName} for ${agentName}: ${response.statusText}`
33
165
  );
@@ -36,8 +168,10 @@ async function fetchAgentFile(agentName, fileName) {
36
168
  }
37
169
  async function fetchRecipeFile(recipeName, fileName) {
38
170
  const url = `${getRegistryUrl()}/recipes/${recipeName}/${fileName}`;
171
+ logger.debug(`Fetching recipe file`, { recipeName, fileName, url });
39
172
  const response = await fetch(url);
40
173
  if (!response.ok) {
174
+ logger.error(`Failed to fetch recipe file`, { recipeName, fileName, url, status: response.status });
41
175
  throw new Error(
42
176
  `Failed to fetch ${fileName} for recipe ${recipeName}: ${response.statusText}`
43
177
  );
@@ -46,26 +180,26 @@ async function fetchRecipeFile(recipeName, fileName) {
46
180
  }
47
181
  function findOpenClawDir(customDir) {
48
182
  if (customDir) {
49
- const resolved = path.resolve(customDir);
50
- if (fs.existsSync(resolved)) {
183
+ const resolved = path2.resolve(customDir);
184
+ if (fs2.existsSync(resolved)) {
51
185
  return resolved;
52
186
  }
53
187
  return resolved;
54
188
  }
55
189
  const envHome = process.env.OPENCLAW_HOME;
56
190
  if (envHome) {
57
- const resolved = path.resolve(envHome);
58
- if (fs.existsSync(resolved)) {
191
+ const resolved = path2.resolve(envHome);
192
+ if (fs2.existsSync(resolved)) {
59
193
  return resolved;
60
194
  }
61
195
  return resolved;
62
196
  }
63
197
  const candidates = [
64
- path.join(process.env.HOME || "~", ".openclaw"),
65
- path.join(process.cwd(), ".openclaw")
198
+ path2.join(process.env.HOME || "~", ".openclaw"),
199
+ path2.join(process.cwd(), ".openclaw")
66
200
  ];
67
201
  for (const candidate of candidates) {
68
- if (fs.existsSync(candidate)) {
202
+ if (fs2.existsSync(candidate)) {
69
203
  return candidate;
70
204
  }
71
205
  }
@@ -73,12 +207,12 @@ function findOpenClawDir(customDir) {
73
207
  }
74
208
  function getConfigPath() {
75
209
  const home = process.env.HOME || "~";
76
- return path.join(home, ".soulhub", "config.json");
210
+ return path2.join(home, ".soulhub", "config.json");
77
211
  }
78
212
  function loadConfig() {
79
213
  const configPath = getConfigPath();
80
- if (fs.existsSync(configPath)) {
81
- return JSON.parse(fs.readFileSync(configPath, "utf-8"));
214
+ if (fs2.existsSync(configPath)) {
215
+ return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
82
216
  }
83
217
  return {
84
218
  installed: [],
@@ -87,11 +221,11 @@ function loadConfig() {
87
221
  }
88
222
  function saveConfig(config) {
89
223
  const configPath = getConfigPath();
90
- const configDir = path.dirname(configPath);
91
- if (!fs.existsSync(configDir)) {
92
- fs.mkdirSync(configDir, { recursive: true });
224
+ const configDir = path2.dirname(configPath);
225
+ if (!fs2.existsSync(configDir)) {
226
+ fs2.mkdirSync(configDir, { recursive: true });
93
227
  }
94
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
228
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
95
229
  }
96
230
  function recordInstall(name, version, workspace) {
97
231
  const config = loadConfig();
@@ -103,6 +237,7 @@ function recordInstall(name, version, workspace) {
103
237
  workspace
104
238
  });
105
239
  saveConfig(config);
240
+ logger.debug(`Recorded install`, { name, version, workspace });
106
241
  }
107
242
  function removeInstallRecord(name) {
108
243
  const config = loadConfig();
@@ -110,17 +245,17 @@ function removeInstallRecord(name) {
110
245
  saveConfig(config);
111
246
  }
112
247
  function getWorkspaceDir(clawDir, agentName) {
113
- return path.join(clawDir, `workspace-${agentName}`);
248
+ return path2.join(clawDir, `workspace-${agentName}`);
114
249
  }
115
250
  function getMainWorkspaceDir(clawDir) {
116
- return path.join(clawDir, "workspace");
251
+ return path2.join(clawDir, "workspace");
117
252
  }
118
253
  function checkMainAgentExists(clawDir) {
119
254
  const workspaceDir = getMainWorkspaceDir(clawDir);
120
- if (!fs.existsSync(workspaceDir)) {
255
+ if (!fs2.existsSync(workspaceDir)) {
121
256
  return { exists: false, hasContent: false, workspaceDir };
122
257
  }
123
- const entries = fs.readdirSync(workspaceDir);
258
+ const entries = fs2.readdirSync(workspaceDir);
124
259
  const hasIdentity = entries.includes("IDENTITY.md");
125
260
  const hasSoul = entries.includes("SOUL.md");
126
261
  return {
@@ -130,22 +265,22 @@ function checkMainAgentExists(clawDir) {
130
265
  };
131
266
  }
132
267
  function getOpenClawConfigPath(clawDir) {
133
- return path.join(clawDir, "openclaw.json");
268
+ return path2.join(clawDir, "openclaw.json");
134
269
  }
135
270
  function readOpenClawConfig(clawDir) {
136
271
  const configPath = getOpenClawConfigPath(clawDir);
137
- if (!fs.existsSync(configPath)) {
272
+ if (!fs2.existsSync(configPath)) {
138
273
  return null;
139
274
  }
140
275
  try {
141
- return JSON.parse(fs.readFileSync(configPath, "utf-8"));
276
+ return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
142
277
  } catch {
143
278
  return null;
144
279
  }
145
280
  }
146
281
  function writeOpenClawConfig(clawDir, config) {
147
282
  const configPath = getOpenClawConfigPath(clawDir);
148
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
283
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
149
284
  }
150
285
  function updateOpenClawConfig(clawDir, updater) {
151
286
  const config = readOpenClawConfig(clawDir);
@@ -198,20 +333,20 @@ function addAgentToOpenClawConfig(clawDir, agentId, agentName, isMain) {
198
333
  config.agents.list.push({
199
334
  id: agentId,
200
335
  name: agentName,
201
- workspace: path.join(clawDir, `workspace-${agentId}`),
202
- agentDir: path.join(clawDir, `agents/${agentId}/agent`)
336
+ workspace: path2.join(clawDir, `workspace-${agentId}`),
337
+ agentDir: path2.join(clawDir, `agents/${agentId}/agent`)
203
338
  });
204
339
  }
205
340
  return config;
206
341
  });
207
342
  }
208
343
  function readSoulHubPackage(dir) {
209
- const yamlPath = path.join(dir, "soulhub.yaml");
210
- if (!fs.existsSync(yamlPath)) {
344
+ const yamlPath = path2.join(dir, "soulhub.yaml");
345
+ if (!fs2.existsSync(yamlPath)) {
211
346
  return null;
212
347
  }
213
348
  try {
214
- return yaml.load(fs.readFileSync(yamlPath, "utf-8"));
349
+ return yaml.load(fs2.readFileSync(yamlPath, "utf-8"));
215
350
  } catch {
216
351
  return null;
217
352
  }
@@ -221,7 +356,7 @@ function detectPackageKind(dir) {
221
356
  if (pkg) {
222
357
  return pkg.kind;
223
358
  }
224
- if (fs.existsSync(path.join(dir, "IDENTITY.md"))) {
359
+ if (fs2.existsSync(path2.join(dir, "IDENTITY.md"))) {
225
360
  return "agent";
226
361
  }
227
362
  return "unknown";
@@ -253,27 +388,136 @@ function checkOpenClawInstalled(customDir) {
253
388
  };
254
389
  }
255
390
  function backupAgentWorkspace(workspaceDir) {
256
- if (!fs.existsSync(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)) {
257
415
  return null;
258
416
  }
259
- const entries = fs.readdirSync(workspaceDir);
417
+ const entries = fs2.readdirSync(workspaceDir);
260
418
  if (entries.length === 0) {
261
419
  return null;
262
420
  }
263
- const clawDir = path.dirname(workspaceDir);
264
- const backupBaseDir = path.join(clawDir, "agentbackup");
265
- if (!fs.existsSync(backupBaseDir)) {
266
- fs.mkdirSync(backupBaseDir, { recursive: true });
421
+ const clawDir = path2.dirname(workspaceDir);
422
+ const backupBaseDir = path2.join(clawDir, "agentbackup");
423
+ if (!fs2.existsSync(backupBaseDir)) {
424
+ fs2.mkdirSync(backupBaseDir, { recursive: true });
267
425
  }
268
- const dirName = path.basename(workspaceDir);
269
- let backupDir = path.join(backupBaseDir, dirName);
270
- if (fs.existsSync(backupDir)) {
426
+ const dirName = path2.basename(workspaceDir);
427
+ let backupDir = path2.join(backupBaseDir, dirName);
428
+ if (fs2.existsSync(backupDir)) {
271
429
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
272
- backupDir = path.join(backupBaseDir, `${dirName}-${timestamp}`);
430
+ backupDir = path2.join(backupBaseDir, `${dirName}-${timestamp}`);
273
431
  }
274
- fs.cpSync(workspaceDir, backupDir, { recursive: true });
432
+ fs2.renameSync(workspaceDir, backupDir);
433
+ logger.info(`Workspace moved to backup`, { from: workspaceDir, to: backupDir });
275
434
  return backupDir;
276
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
+ }
277
521
  var CATEGORY_LABELS = {
278
522
  "self-media": "Self Media",
279
523
  development: "Development",
@@ -284,6 +528,7 @@ var CATEGORY_LABELS = {
284
528
  };
285
529
  function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
286
530
  const agentId = agentName.toLowerCase().replace(/[\s_]+/g, "-");
531
+ logger.debug(`Registering agent to OpenClaw`, { agentId, workspaceDir });
287
532
  try {
288
533
  execSync(
289
534
  `openclaw agents add "${agentId}" --workspace "${workspaceDir}" --non-interactive --json`,
@@ -303,12 +548,14 @@ function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
303
548
  }
304
549
  const isCommandNotFound = cliError && typeof cliError === "object" && "code" in cliError && cliError.code === "ENOENT" || stderr.includes("not found") || stderr.includes("not recognized");
305
550
  if (isCommandNotFound) {
551
+ logger.error(`OpenClaw CLI not found`);
306
552
  return {
307
553
  success: false,
308
554
  message: "OpenClaw CLI not found. Please install OpenClaw first: https://github.com/anthropics/openclaw"
309
555
  };
310
556
  }
311
557
  const errMsg = cliError instanceof Error ? cliError.message : String(cliError);
558
+ logger.error(`openclaw agents add failed`, { agentId, stderr, error: errMsg });
312
559
  return {
313
560
  success: false,
314
561
  message: `openclaw agents add failed: ${stderr || errMsg}`
@@ -316,6 +563,7 @@ function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
316
563
  }
317
564
  }
318
565
  function restartOpenClawGateway() {
566
+ logger.debug(`Restarting OpenClaw Gateway`);
319
567
  try {
320
568
  execSync("openclaw gateway restart", {
321
569
  stdio: "pipe",
@@ -393,9 +641,11 @@ var searchCommand = new Command("search").description("Search for agent template
393
641
  );
394
642
  console.log();
395
643
  } catch (error) {
644
+ logger.errorObj("Search command failed", error);
396
645
  console.error(
397
646
  chalk.red(`Error: ${error instanceof Error ? error.message : error}`)
398
647
  );
648
+ console.error(chalk.dim(` See logs: ${logger.getTodayLogFile()}`));
399
649
  process.exit(1);
400
650
  }
401
651
  });
@@ -466,9 +716,11 @@ var infoCommand = new Command2("info").description("View detailed information ab
466
716
  );
467
717
  console.log();
468
718
  } catch (error) {
719
+ logger.errorObj("Info command failed", error);
469
720
  console.error(
470
721
  chalk2.red(`Error: ${error instanceof Error ? error.message : error}`)
471
722
  );
723
+ console.error(chalk2.dim(` See logs: ${logger.getTodayLogFile()}`));
472
724
  process.exit(1);
473
725
  }
474
726
  });
@@ -477,8 +729,8 @@ var infoCommand = new Command2("info").description("View detailed information ab
477
729
  import { Command as Command3 } from "commander";
478
730
  import chalk3 from "chalk";
479
731
  import ora from "ora";
480
- import fs2 from "fs";
481
- import path2 from "path";
732
+ import fs3 from "fs";
733
+ import path3 from "path";
482
734
  import yaml2 from "js-yaml";
483
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(
484
736
  "--dir <path>",
@@ -501,9 +753,11 @@ var installCommand = new Command3("install").description("Install an agent or te
501
753
  process.exit(1);
502
754
  }
503
755
  } catch (error) {
756
+ logger.errorObj("Install command failed", error);
504
757
  console.error(
505
758
  chalk3.red(`Error: ${error instanceof Error ? error.message : error}`)
506
759
  );
760
+ console.error(chalk3.dim(` See logs: ${logger.getTodayLogFile()}`));
507
761
  process.exit(1);
508
762
  }
509
763
  });
@@ -514,11 +768,14 @@ async function installFromRegistry(name, targetDir, clawDir) {
514
768
  const recipe = index.recipes.find((r) => r.name === name);
515
769
  if (agent && !recipe) {
516
770
  spinner.stop();
771
+ logger.info(`Installing single agent from registry: ${name}`);
517
772
  await installSingleAgent(name, targetDir, clawDir);
518
773
  } else if (recipe) {
519
774
  spinner.stop();
775
+ logger.info(`Installing team recipe from registry: ${name}`);
520
776
  await installRecipeFromRegistry(name, recipe, targetDir, clawDir);
521
777
  } else {
778
+ logger.warn(`"${name}" not found in registry`);
522
779
  spinner.fail(`"${name}" not found in registry.`);
523
780
  console.log(chalk3.dim(" Use 'soulhub search' to find available agents and teams."));
524
781
  }
@@ -544,7 +801,7 @@ async function installSingleAgent(name, targetDir, clawDir) {
544
801
  }
545
802
  let workspaceDir;
546
803
  if (targetDir) {
547
- workspaceDir = path2.resolve(targetDir);
804
+ workspaceDir = path3.resolve(targetDir);
548
805
  } else {
549
806
  const resolvedClawDir = findOpenClawDir(clawDir);
550
807
  if (!resolvedClawDir) {
@@ -554,9 +811,10 @@ async function installSingleAgent(name, targetDir, clawDir) {
554
811
  }
555
812
  workspaceDir = getMainWorkspaceDir(resolvedClawDir);
556
813
  }
814
+ const resolvedClawDirForBackup = findOpenClawDir(clawDir);
815
+ const backupRecord = !targetDir ? createBackupRecord("single-agent", name, resolvedClawDirForBackup) : null;
557
816
  if (!targetDir) {
558
- const resolvedClawDir = findOpenClawDir(clawDir);
559
- const mainCheck = checkMainAgentExists(resolvedClawDir);
817
+ const mainCheck = checkMainAgentExists(resolvedClawDirForBackup);
560
818
  if (mainCheck.hasContent) {
561
819
  spinner.warn(
562
820
  `Existing main agent detected. Backing up workspace...`
@@ -564,6 +822,13 @@ async function installSingleAgent(name, targetDir, clawDir) {
564
822
  const backupDir = backupAgentWorkspace(workspaceDir);
565
823
  if (backupDir) {
566
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
+ });
567
832
  }
568
833
  }
569
834
  } else {
@@ -572,8 +837,8 @@ async function installSingleAgent(name, targetDir, clawDir) {
572
837
  console.log(chalk3.yellow(` \u26A0 Existing agent backed up to: ${backupDir}`));
573
838
  }
574
839
  }
575
- if (!fs2.existsSync(workspaceDir)) {
576
- fs2.mkdirSync(workspaceDir, { recursive: true });
840
+ if (!fs3.existsSync(workspaceDir)) {
841
+ fs3.mkdirSync(workspaceDir, { recursive: true });
577
842
  }
578
843
  if (!targetDir) {
579
844
  spinner.text = `Registering ${chalk3.cyan(agent.displayName)} as main agent...`;
@@ -585,6 +850,11 @@ async function installSingleAgent(name, targetDir, clawDir) {
585
850
  await downloadAgentFiles(name, workspaceDir, spinner);
586
851
  await saveAgentManifest(name, agent, workspaceDir);
587
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 });
588
858
  spinner.succeed(
589
859
  `${chalk3.cyan.bold(agent.displayName)} installed as main agent!`
590
860
  );
@@ -607,7 +877,7 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
607
877
  return;
608
878
  }
609
879
  }
610
- const resolvedClawDir = targetDir ? path2.resolve(targetDir) : findOpenClawDir(clawDir);
880
+ const resolvedClawDir = targetDir ? path3.resolve(targetDir) : findOpenClawDir(clawDir);
611
881
  if (!resolvedClawDir) {
612
882
  spinner.fail("OpenClaw workspace directory not found.");
613
883
  printOpenClawInstallHelp();
@@ -622,9 +892,29 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
622
892
  spinner.fail(`Failed to fetch soulhub.yaml for recipe "${name}". Recipe packages must include a soulhub.yaml file.`);
623
893
  return;
624
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
+ }
625
915
  if (pkg.dispatcher) {
626
916
  spinner.text = `Installing dispatcher ${chalk3.blue(pkg.dispatcher.name)}...`;
627
- await installDispatcher(pkg.dispatcher, resolvedClawDir, clawDir, targetDir, spinner);
917
+ await installDispatcher(pkg.dispatcher, resolvedClawDir, clawDir, targetDir, spinner, recipeBackupRecord);
628
918
  }
629
919
  const index = await fetchIndex();
630
920
  const workerIds = [];
@@ -632,11 +922,7 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
632
922
  spinner.text = `Installing worker ${chalk3.cyan(worker.name)}...`;
633
923
  const agentName = worker.dir || worker.name;
634
924
  const agentId = worker.name;
635
- const workerDir = targetDir ? path2.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
636
- const backupDir = backupAgentWorkspace(workerDir);
637
- if (backupDir) {
638
- console.log(chalk3.yellow(` \u26A0 Existing ${agentId} backed up to: ${backupDir}`));
639
- }
925
+ const workerDir = targetDir ? path3.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
640
926
  if (!targetDir) {
641
927
  const regResult = registerAgentToOpenClaw(agentId, workerDir, clawDir);
642
928
  if (!regResult.success) {
@@ -644,7 +930,7 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
644
930
  continue;
645
931
  }
646
932
  } else {
647
- fs2.mkdirSync(workerDir, { recursive: true });
933
+ fs3.mkdirSync(workerDir, { recursive: true });
648
934
  }
649
935
  await downloadAgentFiles(agentName, workerDir, spinner);
650
936
  const agentInfo = index.agents.find((a) => a.name === agentName);
@@ -659,6 +945,12 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
659
945
  const dispatcherId = "main";
660
946
  configureMultiAgentCommunication(resolvedClawDir, dispatcherId, workerIds);
661
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 });
662
954
  spinner.succeed(
663
955
  `Team ${chalk3.cyan.bold(recipe.displayName)} installed! (1 dispatcher + ${workerIds.length} workers)`
664
956
  );
@@ -672,42 +964,52 @@ async function installFromSource(source, targetDir, clawDir) {
672
964
  let packageDir;
673
965
  let tempDir = null;
674
966
  if (source.startsWith("http://") || source.startsWith("https://")) {
967
+ logger.info(`Downloading package from URL: ${source}`);
675
968
  spinner.text = "Downloading package...";
676
- const response = await fetch(source);
969
+ const response = await fetch(source, {
970
+ headers: {
971
+ "User-Agent": "soulhub-cli",
972
+ "Accept": "application/zip, application/octet-stream"
973
+ }
974
+ });
677
975
  if (!response.ok) {
976
+ logger.error(`Download failed`, { url: source, status: response.status, statusText: response.statusText });
678
977
  spinner.fail(`Failed to download: ${response.statusText}`);
679
978
  return;
680
979
  }
681
980
  const contentType = response.headers.get("content-type") || "";
981
+ logger.debug(`Response content-type: ${contentType}`);
682
982
  if (contentType.includes("zip") || source.endsWith(".zip")) {
683
983
  const JSZip = (await import("jszip")).default;
684
984
  const arrayBuffer = await response.arrayBuffer();
685
985
  const zip = await JSZip.loadAsync(arrayBuffer);
686
- tempDir = path2.join(process.env.HOME || "/tmp", ".soulhub", "tmp", `pkg-${Date.now()}`);
687
- fs2.mkdirSync(tempDir, { recursive: true });
986
+ tempDir = path3.join(process.env.HOME || "/tmp", ".soulhub", "tmp", `pkg-${Date.now()}`);
987
+ fs3.mkdirSync(tempDir, { recursive: true });
688
988
  await extractZipToDir(zip, tempDir);
689
989
  packageDir = tempDir;
690
990
  } else {
991
+ logger.error(`Unsupported content type from URL`, { url: source, contentType });
691
992
  spinner.fail("Unsupported URL content type. Expected ZIP file.");
692
993
  return;
693
994
  }
694
995
  } else if (source.endsWith(".zip")) {
695
996
  spinner.text = "Extracting ZIP file...";
696
997
  const JSZip = (await import("jszip")).default;
697
- const zipData = fs2.readFileSync(path2.resolve(source));
998
+ const zipData = fs3.readFileSync(path3.resolve(source));
698
999
  const zip = await JSZip.loadAsync(zipData);
699
- tempDir = path2.join(process.env.HOME || "/tmp", ".soulhub", "tmp", `pkg-${Date.now()}`);
700
- fs2.mkdirSync(tempDir, { recursive: true });
1000
+ tempDir = path3.join(process.env.HOME || "/tmp", ".soulhub", "tmp", `pkg-${Date.now()}`);
1001
+ fs3.mkdirSync(tempDir, { recursive: true });
701
1002
  await extractZipToDir(zip, tempDir);
702
1003
  packageDir = tempDir;
703
1004
  } else {
704
- packageDir = path2.resolve(source);
705
- if (!fs2.existsSync(packageDir)) {
1005
+ packageDir = path3.resolve(source);
1006
+ if (!fs3.existsSync(packageDir)) {
706
1007
  spinner.fail(`Directory not found: ${packageDir}`);
707
1008
  return;
708
1009
  }
709
1010
  }
710
1011
  const kind = detectPackageKind(packageDir);
1012
+ logger.info(`Detected package type: ${kind}`, { packageDir });
711
1013
  spinner.text = `Detected package type: ${chalk3.blue(kind)}`;
712
1014
  if (kind === "agent") {
713
1015
  spinner.stop();
@@ -716,16 +1018,17 @@ async function installFromSource(source, targetDir, clawDir) {
716
1018
  spinner.stop();
717
1019
  await installTeamFromDir(packageDir, targetDir, clawDir);
718
1020
  } else {
1021
+ logger.error(`Cannot determine package type`, { packageDir, files: fs3.existsSync(packageDir) ? fs3.readdirSync(packageDir) : [] });
719
1022
  spinner.fail("Cannot determine package type. Expected soulhub.yaml or IDENTITY.md.");
720
1023
  }
721
- if (tempDir && fs2.existsSync(tempDir)) {
722
- fs2.rmSync(tempDir, { recursive: true, force: true });
1024
+ if (tempDir && fs3.existsSync(tempDir)) {
1025
+ fs3.rmSync(tempDir, { recursive: true, force: true });
723
1026
  }
724
1027
  }
725
1028
  async function installSingleAgentFromDir(packageDir, targetDir, clawDir) {
726
1029
  const spinner = ora("Installing single agent...").start();
727
1030
  const pkg = readSoulHubPackage(packageDir);
728
- const agentName = pkg?.name || path2.basename(packageDir);
1031
+ const agentName = pkg?.name || path3.basename(packageDir);
729
1032
  if (!targetDir) {
730
1033
  const clawCheck = checkOpenClawInstalled(clawDir);
731
1034
  if (!clawCheck.installed) {
@@ -736,7 +1039,7 @@ async function installSingleAgentFromDir(packageDir, targetDir, clawDir) {
736
1039
  }
737
1040
  let workspaceDir;
738
1041
  if (targetDir) {
739
- workspaceDir = path2.resolve(targetDir);
1042
+ workspaceDir = path3.resolve(targetDir);
740
1043
  } else {
741
1044
  const resolvedClawDir = findOpenClawDir(clawDir);
742
1045
  if (!resolvedClawDir) {
@@ -745,6 +1048,7 @@ async function installSingleAgentFromDir(packageDir, targetDir, clawDir) {
745
1048
  }
746
1049
  workspaceDir = getMainWorkspaceDir(resolvedClawDir);
747
1050
  }
1051
+ const localBackupRecord = !targetDir ? createBackupRecord("single-agent-local", agentName, findOpenClawDir(clawDir)) : null;
748
1052
  if (!targetDir) {
749
1053
  const resolvedClawDir = findOpenClawDir(clawDir);
750
1054
  const mainCheck = checkMainAgentExists(resolvedClawDir);
@@ -753,11 +1057,18 @@ async function installSingleAgentFromDir(packageDir, targetDir, clawDir) {
753
1057
  const backupDir = backupAgentWorkspace(workspaceDir);
754
1058
  if (backupDir) {
755
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
+ });
756
1067
  }
757
1068
  }
758
1069
  }
759
- if (!fs2.existsSync(workspaceDir)) {
760
- fs2.mkdirSync(workspaceDir, { recursive: true });
1070
+ if (!fs3.existsSync(workspaceDir)) {
1071
+ fs3.mkdirSync(workspaceDir, { recursive: true });
761
1072
  }
762
1073
  if (!targetDir) {
763
1074
  spinner.text = `Registering ${chalk3.cyan(agentName)} as main agent...`;
@@ -767,6 +1078,11 @@ async function installSingleAgentFromDir(packageDir, targetDir, clawDir) {
767
1078
  spinner.text = `Copying soul files...`;
768
1079
  copyAgentFilesFromDir(packageDir, workspaceDir);
769
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 });
770
1086
  spinner.succeed(`${chalk3.cyan.bold(agentName)} installed as main agent!`);
771
1087
  console.log();
772
1088
  console.log(` ${chalk3.dim("Location:")} ${workspaceDir}`);
@@ -792,14 +1108,34 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
792
1108
  return;
793
1109
  }
794
1110
  }
795
- const resolvedClawDir = targetDir ? path2.resolve(targetDir) : findOpenClawDir(clawDir);
1111
+ const resolvedClawDir = targetDir ? path3.resolve(targetDir) : findOpenClawDir(clawDir);
796
1112
  if (!resolvedClawDir) {
797
1113
  spinner.fail("OpenClaw workspace directory not found.");
798
1114
  return;
799
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
+ }
800
1136
  if (pkg.dispatcher) {
801
1137
  spinner.text = `Installing dispatcher ${chalk3.blue(pkg.dispatcher.name)}...`;
802
- const mainWorkspace = targetDir ? path2.join(resolvedClawDir, "workspace") : getMainWorkspaceDir(resolvedClawDir);
1138
+ const mainWorkspace = targetDir ? path3.join(resolvedClawDir, "workspace") : getMainWorkspaceDir(resolvedClawDir);
803
1139
  if (!targetDir) {
804
1140
  const mainCheck = checkMainAgentExists(resolvedClawDir);
805
1141
  if (mainCheck.hasContent) {
@@ -807,17 +1143,26 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
807
1143
  const backupDir = backupAgentWorkspace(mainWorkspace);
808
1144
  if (backupDir) {
809
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
+ }
810
1155
  }
811
1156
  }
812
1157
  }
813
- if (!fs2.existsSync(mainWorkspace)) {
814
- fs2.mkdirSync(mainWorkspace, { recursive: true });
1158
+ if (!fs3.existsSync(mainWorkspace)) {
1159
+ fs3.mkdirSync(mainWorkspace, { recursive: true });
815
1160
  }
816
1161
  if (!targetDir) {
817
1162
  addAgentToOpenClawConfig(resolvedClawDir, "main", pkg.dispatcher.name, true);
818
1163
  }
819
- const dispatcherSourceDir = path2.join(packageDir, pkg.dispatcher.dir);
820
- if (fs2.existsSync(dispatcherSourceDir)) {
1164
+ const dispatcherSourceDir = path3.join(packageDir, pkg.dispatcher.dir);
1165
+ if (fs3.existsSync(dispatcherSourceDir)) {
821
1166
  copyAgentFilesFromDir(dispatcherSourceDir, mainWorkspace);
822
1167
  }
823
1168
  recordInstall("dispatcher", pkg.version || "local", mainWorkspace);
@@ -827,11 +1172,7 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
827
1172
  const agentId = worker.name;
828
1173
  const agentDir = worker.dir || worker.name;
829
1174
  spinner.text = `Installing worker ${chalk3.cyan(agentId)}...`;
830
- const workerWorkspace = targetDir ? path2.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
831
- const backupDir = backupAgentWorkspace(workerWorkspace);
832
- if (backupDir) {
833
- console.log(chalk3.yellow(` \u26A0 Existing ${agentId} backed up to: ${backupDir}`));
834
- }
1175
+ const workerWorkspace = targetDir ? path3.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
835
1176
  if (!targetDir) {
836
1177
  const regResult = registerAgentToOpenClaw(agentId, workerWorkspace, clawDir);
837
1178
  if (!regResult.success) {
@@ -839,10 +1180,10 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
839
1180
  continue;
840
1181
  }
841
1182
  } else {
842
- fs2.mkdirSync(workerWorkspace, { recursive: true });
1183
+ fs3.mkdirSync(workerWorkspace, { recursive: true });
843
1184
  }
844
- const workerSourceDir = path2.join(packageDir, agentDir);
845
- if (fs2.existsSync(workerSourceDir)) {
1185
+ const workerSourceDir = path3.join(packageDir, agentDir);
1186
+ if (fs3.existsSync(workerSourceDir)) {
846
1187
  copyAgentFilesFromDir(workerSourceDir, workerWorkspace);
847
1188
  }
848
1189
  recordInstall(agentId, pkg.version || "local", workerWorkspace);
@@ -852,6 +1193,12 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
852
1193
  spinner.text = "Configuring multi-agent communication...";
853
1194
  configureMultiAgentCommunication(resolvedClawDir, "main", workerIds);
854
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 });
855
1202
  spinner.succeed(
856
1203
  `Team ${chalk3.cyan.bold(pkg.name)} installed! (${pkg.dispatcher ? "1 dispatcher + " : ""}${workerIds.length} workers)`
857
1204
  );
@@ -861,11 +1208,11 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
861
1208
  }
862
1209
  }
863
1210
  async function downloadAgentFiles(agentName, workspaceDir, spinner) {
864
- fs2.mkdirSync(workspaceDir, { recursive: true });
1211
+ fs3.mkdirSync(workspaceDir, { recursive: true });
865
1212
  const coreFiles = ["IDENTITY.md", "SOUL.md"];
866
1213
  for (const fileName of coreFiles) {
867
1214
  const content = await fetchAgentFile(agentName, fileName);
868
- fs2.writeFileSync(path2.join(workspaceDir, fileName), content);
1215
+ fs3.writeFileSync(path3.join(workspaceDir, fileName), content);
869
1216
  spinner.text = `Downloaded ${fileName}`;
870
1217
  }
871
1218
  const optionalFiles = ["USER.md.template", "TOOLS.md.template"];
@@ -873,7 +1220,7 @@ async function downloadAgentFiles(agentName, workspaceDir, spinner) {
873
1220
  try {
874
1221
  const content = await fetchAgentFile(agentName, fileName);
875
1222
  const actualName = fileName.replace(".template", "");
876
- fs2.writeFileSync(path2.join(workspaceDir, actualName), content);
1223
+ fs3.writeFileSync(path3.join(workspaceDir, actualName), content);
877
1224
  } catch {
878
1225
  }
879
1226
  }
@@ -881,7 +1228,7 @@ async function downloadAgentFiles(agentName, workspaceDir, spinner) {
881
1228
  async function saveAgentManifest(agentName, agent, workspaceDir) {
882
1229
  try {
883
1230
  const manifestContent = await fetchAgentFile(agentName, "manifest.yaml");
884
- fs2.writeFileSync(path2.join(workspaceDir, "manifest.yaml"), manifestContent);
1231
+ fs3.writeFileSync(path3.join(workspaceDir, "manifest.yaml"), manifestContent);
885
1232
  } catch {
886
1233
  const manifest = {
887
1234
  name: agent.name,
@@ -892,21 +1239,21 @@ async function saveAgentManifest(agentName, agent, workspaceDir) {
892
1239
  version: agent.version,
893
1240
  author: agent.author
894
1241
  };
895
- fs2.writeFileSync(path2.join(workspaceDir, "manifest.yaml"), yaml2.dump(manifest));
1242
+ fs3.writeFileSync(path3.join(workspaceDir, "manifest.yaml"), yaml2.dump(manifest));
896
1243
  }
897
1244
  }
898
1245
  function copyAgentFilesFromDir(sourceDir, targetDir) {
899
1246
  const filesToCopy = ["IDENTITY.md", "SOUL.md", "USER.md", "TOOLS.md", "AGENTS.md", "HEARTBEAT.md"];
900
1247
  for (const fileName of filesToCopy) {
901
- const sourcePath = path2.join(sourceDir, fileName);
902
- if (fs2.existsSync(sourcePath)) {
903
- fs2.mkdirSync(targetDir, { recursive: true });
904
- fs2.copyFileSync(sourcePath, path2.join(targetDir, fileName));
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));
905
1252
  }
906
1253
  }
907
1254
  }
908
- async function installDispatcher(dispatcher, resolvedClawDir, clawDir, targetDir, spinner) {
909
- const mainWorkspace = targetDir ? path2.join(resolvedClawDir, "workspace") : getMainWorkspaceDir(resolvedClawDir);
1255
+ async function installDispatcher(dispatcher, resolvedClawDir, clawDir, targetDir, spinner, backupRecord) {
1256
+ const mainWorkspace = targetDir ? path3.join(resolvedClawDir, "workspace") : getMainWorkspaceDir(resolvedClawDir);
910
1257
  if (!targetDir) {
911
1258
  const mainCheck = checkMainAgentExists(resolvedClawDir);
912
1259
  if (mainCheck.hasContent) {
@@ -914,11 +1261,20 @@ async function installDispatcher(dispatcher, resolvedClawDir, clawDir, targetDir
914
1261
  const backupDir = backupAgentWorkspace(mainWorkspace);
915
1262
  if (backupDir) {
916
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
+ }
917
1273
  }
918
1274
  }
919
1275
  }
920
- if (!fs2.existsSync(mainWorkspace)) {
921
- fs2.mkdirSync(mainWorkspace, { recursive: true });
1276
+ if (!fs3.existsSync(mainWorkspace)) {
1277
+ fs3.mkdirSync(mainWorkspace, { recursive: true });
922
1278
  }
923
1279
  if (!targetDir) {
924
1280
  addAgentToOpenClawConfig(resolvedClawDir, "main", dispatcher.name, true);
@@ -931,13 +1287,13 @@ async function installDispatcher(dispatcher, resolvedClawDir, clawDir, targetDir
931
1287
  async function extractZipToDir(zip, targetDir) {
932
1288
  const entries = Object.entries(zip.files);
933
1289
  for (const [relativePath, file] of entries) {
934
- const fullPath = path2.join(targetDir, relativePath);
1290
+ const fullPath = path3.join(targetDir, relativePath);
935
1291
  if (file.dir) {
936
- fs2.mkdirSync(fullPath, { recursive: true });
1292
+ fs3.mkdirSync(fullPath, { recursive: true });
937
1293
  } else {
938
- fs2.mkdirSync(path2.dirname(fullPath), { recursive: true });
1294
+ fs3.mkdirSync(path3.dirname(fullPath), { recursive: true });
939
1295
  const content = await file.async("nodebuffer");
940
- fs2.writeFileSync(fullPath, content);
1296
+ fs3.writeFileSync(fullPath, content);
941
1297
  }
942
1298
  }
943
1299
  }
@@ -1008,9 +1364,11 @@ var listCommand = new Command4("list").description("List installed agent templat
1008
1364
  console.log();
1009
1365
  }
1010
1366
  } catch (error) {
1367
+ logger.errorObj("List command failed", error);
1011
1368
  console.error(
1012
1369
  chalk4.red(`Error: ${error instanceof Error ? error.message : error}`)
1013
1370
  );
1371
+ console.error(chalk4.dim(` See logs: ${logger.getTodayLogFile()}`));
1014
1372
  process.exit(1);
1015
1373
  }
1016
1374
  });
@@ -1019,8 +1377,8 @@ var listCommand = new Command4("list").description("List installed agent templat
1019
1377
  import { Command as Command5 } from "commander";
1020
1378
  import chalk5 from "chalk";
1021
1379
  import ora2 from "ora";
1022
- import fs3 from "fs";
1023
- import path3 from "path";
1380
+ import fs4 from "fs";
1381
+ import path4 from "path";
1024
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) => {
1025
1383
  try {
1026
1384
  const config = loadConfig();
@@ -1049,8 +1407,8 @@ var updateCommand = new Command5("update").description("Update installed agent t
1049
1407
  }
1050
1408
  spinner.text = `Updating ${chalk5.cyan(installed.name)} (${installed.version} \u2192 ${remote.version})...`;
1051
1409
  const workspaceDir = installed.workspace;
1052
- if (!fs3.existsSync(workspaceDir)) {
1053
- fs3.mkdirSync(workspaceDir, { recursive: true });
1410
+ if (!fs4.existsSync(workspaceDir)) {
1411
+ fs4.mkdirSync(workspaceDir, { recursive: true });
1054
1412
  }
1055
1413
  for (const fileName of ["IDENTITY.md", "SOUL.md", "manifest.yaml"]) {
1056
1414
  try {
@@ -1058,8 +1416,8 @@ var updateCommand = new Command5("update").description("Update installed agent t
1058
1416
  installed.name,
1059
1417
  fileName
1060
1418
  );
1061
- fs3.writeFileSync(
1062
- path3.join(workspaceDir, fileName),
1419
+ fs4.writeFileSync(
1420
+ path4.join(workspaceDir, fileName),
1063
1421
  content
1064
1422
  );
1065
1423
  } catch {
@@ -1073,13 +1431,16 @@ var updateCommand = new Command5("update").description("Update installed agent t
1073
1431
  if (updated === 0) {
1074
1432
  spinner.succeed("All agents are up to date.");
1075
1433
  } else {
1434
+ logger.info(`Updated ${updated} agent(s)`);
1076
1435
  spinner.succeed(`Updated ${updated} agent(s).`);
1077
1436
  }
1078
1437
  console.log();
1079
1438
  } catch (error) {
1439
+ logger.errorObj("Update command failed", error);
1080
1440
  console.error(
1081
1441
  chalk5.red(`Error: ${error instanceof Error ? error.message : error}`)
1082
1442
  );
1443
+ console.error(chalk5.dim(` See logs: ${logger.getTodayLogFile()}`));
1083
1444
  process.exit(1);
1084
1445
  }
1085
1446
  });
@@ -1088,7 +1449,7 @@ var updateCommand = new Command5("update").description("Update installed agent t
1088
1449
  import { Command as Command6 } from "commander";
1089
1450
  import chalk6 from "chalk";
1090
1451
  import ora3 from "ora";
1091
- import fs4 from "fs";
1452
+ import fs5 from "fs";
1092
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) => {
1093
1454
  try {
1094
1455
  const config = loadConfig();
@@ -1107,11 +1468,12 @@ var uninstallCommand = new Command6("uninstall").description("Uninstall an agent
1107
1468
  const spinner = ora3(
1108
1469
  `Uninstalling ${chalk6.cyan(name)}...`
1109
1470
  ).start();
1110
- if (!options.keepFiles && fs4.existsSync(installed.workspace)) {
1111
- fs4.rmSync(installed.workspace, { recursive: true, force: true });
1471
+ if (!options.keepFiles && fs5.existsSync(installed.workspace)) {
1472
+ fs5.rmSync(installed.workspace, { recursive: true, force: true });
1112
1473
  spinner.text = `Removed workspace: ${installed.workspace}`;
1113
1474
  }
1114
1475
  removeInstallRecord(name);
1476
+ logger.info(`Agent uninstalled: ${name}`, { workspace: installed.workspace, keepFiles: !!options.keepFiles });
1115
1477
  spinner.succeed(
1116
1478
  `${chalk6.cyan.bold(name)} uninstalled.`
1117
1479
  );
@@ -1122,9 +1484,11 @@ var uninstallCommand = new Command6("uninstall").description("Uninstall an agent
1122
1484
  }
1123
1485
  console.log();
1124
1486
  } catch (error) {
1487
+ logger.errorObj("Uninstall command failed", error);
1125
1488
  console.error(
1126
1489
  chalk6.red(`Error: ${error instanceof Error ? error.message : error}`)
1127
1490
  );
1491
+ console.error(chalk6.dim(` See logs: ${logger.getTodayLogFile()}`));
1128
1492
  process.exit(1);
1129
1493
  }
1130
1494
  });
@@ -1133,19 +1497,19 @@ var uninstallCommand = new Command6("uninstall").description("Uninstall an agent
1133
1497
  import { Command as Command7 } from "commander";
1134
1498
  import chalk7 from "chalk";
1135
1499
  import ora4 from "ora";
1136
- import fs5 from "fs";
1137
- import path4 from "path";
1500
+ import fs6 from "fs";
1501
+ import path5 from "path";
1138
1502
  import yaml3 from "js-yaml";
1139
1503
  var publishCommand = new Command7("publish").description("Publish an agent template to the SoulHub community").argument(
1140
1504
  "[directory]",
1141
1505
  "Agent workspace directory (defaults to current directory)"
1142
1506
  ).action(async (directory) => {
1143
1507
  try {
1144
- const dir = directory ? path4.resolve(directory) : process.cwd();
1508
+ const dir = directory ? path5.resolve(directory) : process.cwd();
1145
1509
  const spinner = ora4("Validating agent template...").start();
1146
1510
  const requiredFiles = ["manifest.yaml", "IDENTITY.md", "SOUL.md"];
1147
1511
  const missing = requiredFiles.filter(
1148
- (f) => !fs5.existsSync(path4.join(dir, f))
1512
+ (f) => !fs6.existsSync(path5.join(dir, f))
1149
1513
  );
1150
1514
  if (missing.length > 0) {
1151
1515
  spinner.fail("Missing required files:");
@@ -1161,8 +1525,8 @@ var publishCommand = new Command7("publish").description("Publish an agent templ
1161
1525
  );
1162
1526
  return;
1163
1527
  }
1164
- const manifestContent = fs5.readFileSync(
1165
- path4.join(dir, "manifest.yaml"),
1528
+ const manifestContent = fs6.readFileSync(
1529
+ path5.join(dir, "manifest.yaml"),
1166
1530
  "utf-8"
1167
1531
  );
1168
1532
  const manifest = yaml3.load(manifestContent);
@@ -1225,16 +1589,27 @@ var publishCommand = new Command7("publish").description("Publish an agent templ
1225
1589
  );
1226
1590
  console.log();
1227
1591
  } catch (error) {
1592
+ logger.errorObj("Publish command failed", error);
1228
1593
  console.error(
1229
1594
  chalk7.red(`Error: ${error instanceof Error ? error.message : error}`)
1230
1595
  );
1596
+ console.error(chalk7.dim(` See logs: ${logger.getTodayLogFile()}`));
1231
1597
  process.exit(1);
1232
1598
  }
1233
1599
  });
1234
1600
 
1235
1601
  // src/index.ts
1236
1602
  var program = new Command8();
1237
- program.name("soulhub").description("SoulHub CLI - Install and manage AI agent persona templates").version("0.1.0");
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
+ });
1238
1613
  program.addCommand(searchCommand);
1239
1614
  program.addCommand(infoCommand);
1240
1615
  program.addCommand(installCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulhubcli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "SoulHub CLI - Install and manage AI agent persona templates for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {