soulhubcli 1.0.21 → 1.0.22

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 (3) hide show
  1. package/README.md +133 -21
  2. package/dist/index.cjs +601 -98
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,14 +34,41 @@ npx soulhubcli <command>
34
34
 
35
35
  | 命令 | 说明 |
36
36
  |------|------|
37
- | `soulhub search <keyword>` | 搜索 Agent |
37
+ | `soulhub search [query]` | 搜索 Agent 模板(匹配名称、描述、标签) |
38
+ | `soulhub search -c <category>` | 按分类筛选搜索结果 |
39
+ | `soulhub search -n <number>` | 限制搜索结果数量(默认 20) |
40
+ | `soulhub search --json` | 以 JSON 格式输出搜索结果 |
38
41
  | `soulhub info <name>` | 查看 Agent 详细信息(identity、soul、skills 等) |
39
- | `soulhub install <name>` | Registry 安装 Agent 或团队(默认为 worker,安装到所有检测到的 claw) |
40
- | `soulhub install <name> --main` | 安装为主 Agent |
42
+ | `soulhub info <name> --identity` | 显示 IDENTITY.md 内容 |
43
+ | `soulhub info <name> --soul` | 显示 SOUL.md 内容 |
44
+ | `soulhub info <name> --json` | 以 JSON 格式输出 Agent 详情 |
45
+ | `soulhub install <name>` | 从 Registry 安装 Agent 或团队(交互式:选择角色和目标 claw) |
46
+ | `soulhub install <name> --role main` | 安装为主 Agent(跳过角色选择) |
47
+ | `soulhub install <name> --role worker` | 安装为 Worker Agent(跳过角色选择) |
48
+ | `soulhub install <name> --claw-type <type>` | 指定 claw 类型(跳过 claw 选择) |
49
+ | `soulhub install <name> --dir <path>` | 安装到指定目录(不依赖 claw 环境) |
50
+ | `soulhub install <name> -y` | 跳过所有确认提示(自动确认) |
41
51
  | `soulhub install --from <source>` | 从本地目录、ZIP 文件或 URL 安装 |
42
52
  | `soulhub list` | 列出已安装的 Agent |
43
- | `soulhub update [name]` | 更新已安装的 Agent |
44
- | `soulhub rollback` | 回滚到上一次安装状态 |
53
+ | `soulhub list --json` | JSON 格式输出已安装的 Agent |
54
+ | `soulhub update [name]` | 更新已安装的 Agent(不传名称则更新全部) |
55
+ | `soulhub uninstall <name>` | 卸载 Agent(同时删除相关备份) |
56
+ | `soulhub uninstall <name> --keep-files` | 卸载但保留 workspace 文件 |
57
+ | `soulhub uninstall <name> -y` | 卸载 Agent(跳过确认提示) |
58
+ | `soulhub rollback` | 交互式选择回滚到某次安装前的状态 |
59
+ | `soulhub rollback --list` | 列出所有可用的回滚记录 |
60
+ | `soulhub rollback --last <n>` | 回滚倒数第 n 次安装(1 = 最近一次) |
61
+ | `soulhub rollback --id <id>` | 按 ID 回滚到指定的备份记录 |
62
+ | `soulhub rollback --claw-type <type>` | 指定回滚的目标 claw 类型 |
63
+ | `soulhub rollback --last <n> -y` | 回滚并跳过确认提示 |
64
+
65
+ ### 全局选项
66
+
67
+ | 选项 | 说明 |
68
+ |------|------|
69
+ | `--verbose` | 启用详细调试日志 |
70
+ | `--version` | 显示版本号 |
71
+ | `--help` | 显示帮助信息 |
45
72
 
46
73
  ## 使用方法
47
74
 
@@ -50,22 +77,41 @@ npx soulhubcli <command>
50
77
  ```bash
51
78
  soulhub search python
52
79
  soulhub search "content writing"
80
+
81
+ # 按分类筛选
82
+ soulhub search -c development
83
+
84
+ # 限制结果数量
85
+ soulhub search writer -n 5
86
+
87
+ # JSON 格式输出(适合 CI/管道集成)
88
+ soulhub search writer --json
53
89
  ```
54
90
 
55
91
  ### 安装 Agent
56
92
 
57
93
  CLI 会自动识别目标是单 Agent 还是多 Agent 团队,无需手动区分。
58
94
 
59
- **默认行为:安装为 Worker Agent,自动安装到所有检测到的 claw 目录。**
95
+ **默认行为:交互式安装。** CLI 会提示用户选择安装角色(主 Agent / Worker Agent)以及目标 claw 目录(支持多选)。通过命令行参数可跳过交互,实现完全非交互式安装。
60
96
 
61
97
  **从 Registry 安装:**
62
98
 
63
99
  ```bash
64
- # 安装单 Agent(默认为 worker,安装到所有检测到的 claw)
100
+ # 交互式安装(会提示选择角色和 claw 目录)
65
101
  soulhub install writer-wechat
66
102
 
67
- # 安装为主 Agent
68
- soulhub install writer-wechat --main
103
+ # 指定角色,仍交互选择 claw 目录
104
+ soulhub install writer-wechat --role main
105
+ soulhub install writer-wechat --role worker
106
+
107
+ # 指定 claw 类型,仍交互选择角色
108
+ soulhub install writer-wechat --claw-type LightClaw
109
+
110
+ # 完全非交互式安装
111
+ soulhub install writer-wechat --role worker --claw-type OpenClaw
112
+
113
+ # 完全非交互式安装(主 Agent,-y 跳过确认)
114
+ soulhub install writer-wechat --role main --claw-type OpenClaw -y
69
115
 
70
116
  # 安装多 Agent 团队(调度 Agent + 工作 Agent)
71
117
  soulhub install dev-squad
@@ -89,15 +135,27 @@ soulhub install --from https://example.com/agent-team.zip
89
135
  ```bash
90
136
  # 安装到自定义目录(不依赖 OpenClaw/LightClaw 环境)
91
137
  soulhub install writer-wechat --dir ./my-agents
138
+ ```
139
+
140
+ ### 查看 Agent 详情
92
141
 
93
- # 指定 claw 类型(只安装到该 claw)
94
- soulhub install writer-wechat --clawtype LightClaw
142
+ ```bash
143
+ soulhub info writer-xiaohongshu
144
+
145
+ # 查看 IDENTITY.md 和 SOUL.md 内容
146
+ soulhub info writer-xiaohongshu --identity --soul
147
+
148
+ # JSON 格式输出
149
+ soulhub info writer-xiaohongshu --json
95
150
  ```
96
151
 
97
152
  ### 列出已安装的 Agent
98
153
 
99
154
  ```bash
100
155
  soulhub list
156
+
157
+ # JSON 格式输出
158
+ soulhub list --json
101
159
  ```
102
160
 
103
161
  ### 更新 Agent
@@ -109,28 +167,81 @@ soulhub update ops-assistant # 更新指定 Agent
109
167
 
110
168
  ### 卸载 Agent
111
169
 
170
+ 卸载时,如果存在相关备份记录,CLI 会提示用户确认,因为卸载操作会同时删除该 Agent 的所有备份文件,删除后将无法回滚。使用 `-y` 参数可跳过确认。
171
+
112
172
  ```bash
113
173
  soulhub uninstall ops-assistant
174
+
175
+ # 跳过确认提示
176
+ soulhub uninstall ops-assistant -y
177
+ ```
178
+
179
+ 使用 `--keep-files` 参数可保留 workspace 文件,仅从安装记录中移除:
180
+
181
+ ```bash
182
+ soulhub uninstall ops-assistant --keep-files
183
+ ```
184
+
185
+ ### 回滚安装
186
+
187
+ 每次安装操作都会自动创建备份记录,支持回滚到安装前的状态。
188
+
189
+ ```bash
190
+ # 交互式选择:展示所有备份记录,选择要回滚的项
191
+ soulhub rollback
192
+
193
+ # 查看所有可用的回滚记录
194
+ soulhub rollback --list
195
+
196
+ # 非交互式:回滚最近一次安装
197
+ soulhub rollback --last 1
198
+
199
+ # 非交互式:回滚最近一次安装(跳过确认)
200
+ soulhub rollback --last 1 -y
201
+
202
+ # 非交互式:回滚倒数第 2 次安装
203
+ soulhub rollback --last 2
204
+
205
+ # 按 ID 回滚到指定记录
206
+ soulhub rollback --id <record-id>
114
207
  ```
115
208
 
116
209
  ## 安装行为说明
117
210
 
211
+ ### 交互式安装流程
212
+
213
+ 未指定 `--role` 和 `--claw-type` 参数时,CLI 进入交互式安装:
214
+
215
+ 1. 展示 Agent 基本信息(名称、版本、描述、分类、标签)
216
+ 2. 提示选择安装角色:**Main Agent** 或 **Worker Agent**
217
+ 3. 安装为 Main Agent 时,警告将覆盖当前 workspace 内容(人格文件会被替换,记忆不受影响),需用户确认(或使用 `-y` 跳过)
218
+ 4. 提示多选目标 claw 目录(OpenClaw / LightClaw),可同时安装到多个 claw
219
+ 5. 执行安装、注册、重启
220
+
118
221
  ### 单 Agent 安装
119
222
 
120
- - **默认安装为 Worker Agent**(子 agent),部署到 `workspace-<agentId>/` 目录
121
- - 使用 `--main` 参数可安装为主 Agent,部署到 `workspace/` 目录
122
- - **自动安装到所有检测到的 claw 目录**(OpenClaw / LightClaw),使用 `--clawtype` 可指定单个 claw
123
- - 如果目标目录已存在,CLI 会**自动备份**(复制到 `agentbackup/`)
223
+ - 安装为 **Worker Agent** 时,部署到 `workspace-<agentId>/` 目录
224
+ - 安装为 **Main Agent** 时,部署到 `workspace/` 目录,会覆盖已有人格文件
225
+ - 安装前自动备份已有内容到 `~/.soulhub/backups/<claw>/`(按 claw 类型分目录存储)
124
226
  - 仅覆盖 `IDENTITY.md`、`SOUL.md` 等灵魂文件,不影响 workspace 中的其他运行时文件
125
- - 安装完成后自动重启 OpenClaw Gateway;若重启失败会提示手动重启
227
+ - 安装完成后自动重启 OpenClaw/LightClaw Gateway;若重启失败会提示手动重启
126
228
 
127
229
  ### 多 Agent 团队安装
128
230
 
129
231
  - **调度 Agent(Dispatcher)** 作为主 Agent 安装到 `workspace/` 目录
130
232
  - **工作 Agent(Worker)** 安装到各自的 `workspace-<agentId>/` 目录
233
+ - 安装前自动备份存量子 Agent(mv 方式移走已有 worker 目录)
131
234
  - 自动配置多 Agent 之间的通信
132
235
  - Worker Agent 自动注册到 claw 配置中
133
- - 安装完成后自动重启 OpenClaw Gateway
236
+ - 安装完成后自动重启 OpenClaw/LightClaw Gateway
237
+
238
+ ### 备份与回滚
239
+
240
+ - 备份文件存储在 `~/.soulhub/backups/<claw>/` 目录下,按 claw 类型(如 `openclaw`、`lightclaw`)分目录管理
241
+ - 备份清单记录在 `~/.soulhub/backup-manifest.json` 中
242
+ - 每次安装都会自动创建备份记录,包含 agent 文件和 claw 配置快照
243
+ - 回滚时自动恢复备份文件和 claw 配置,并重启 Gateway
244
+ - 卸载 Agent 时会同时清理相关备份记录和备份文件
134
245
 
135
246
  ## 配置
136
247
 
@@ -148,11 +259,12 @@ export SOULHUB_REGISTRY_URL=https://your-registry.example.com
148
259
 
149
260
  CLI 按以下优先级查找 claw 安装目录:
150
261
 
151
- 1. `--clawtype` 命令行参数(指定时只安装到该 claw)
152
- 2. `OPENCLAW_HOME` / `LIGHTCLAW_HOME` 环境变量
153
- 3. 默认路径 `~/.openclaw`、`~/.lightclaw`
262
+ 1. `--claw-type` 命令行参数(指定时只安装到该 claw)
263
+ 2. `--dir` 命令行参数(直接指定目标目录,不依赖 claw 环境)
264
+ 3. `OPENCLAW_HOME` / `LIGHTCLAW_HOME` 环境变量
265
+ 4. 默认路径 `~/.openclaw`、`~/.lightclaw`
154
266
 
155
- 未指定 `--clawtype` 时,CLI 会检测所有可用的 claw 目录并全部安装。
267
+ 未指定 `--claw-type` 或 `--dir` 时,CLI 会检测所有可用的 claw 目录,多个时交互式多选。
156
268
 
157
269
  ## 环境要求
158
270
 
package/dist/index.cjs CHANGED
@@ -966,7 +966,7 @@ var require_command = __commonJS({
966
966
  var EventEmitter = require("events").EventEmitter;
967
967
  var childProcess = require("child_process");
968
968
  var path5 = require("path");
969
- var fs7 = require("fs");
969
+ var fs8 = require("fs");
970
970
  var process3 = require("process");
971
971
  var { Argument: Argument2, humanReadableArgName } = require_argument();
972
972
  var { CommanderError: CommanderError2 } = require_error();
@@ -1899,10 +1899,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
1899
1899
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1900
1900
  function findFile(baseDir, baseName) {
1901
1901
  const localBin = path5.resolve(baseDir, baseName);
1902
- if (fs7.existsSync(localBin)) return localBin;
1902
+ if (fs8.existsSync(localBin)) return localBin;
1903
1903
  if (sourceExt.includes(path5.extname(baseName))) return void 0;
1904
1904
  const foundExt = sourceExt.find(
1905
- (ext) => fs7.existsSync(`${localBin}${ext}`)
1905
+ (ext) => fs8.existsSync(`${localBin}${ext}`)
1906
1906
  );
1907
1907
  if (foundExt) return `${localBin}${foundExt}`;
1908
1908
  return void 0;
@@ -1914,7 +1914,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1914
1914
  if (this._scriptPath) {
1915
1915
  let resolvedScriptPath;
1916
1916
  try {
1917
- resolvedScriptPath = fs7.realpathSync(this._scriptPath);
1917
+ resolvedScriptPath = fs8.realpathSync(this._scriptPath);
1918
1918
  } catch (err) {
1919
1919
  resolvedScriptPath = this._scriptPath;
1920
1920
  }
@@ -19165,6 +19165,11 @@ function recordInstall(name, version, workspace) {
19165
19165
  saveConfig(config);
19166
19166
  logger.debug(`Recorded install`, { name, version, workspace });
19167
19167
  }
19168
+ function removeInstallRecord(name) {
19169
+ const config = loadConfig();
19170
+ config.installed = config.installed.filter((a) => a.name !== name);
19171
+ saveConfig(config);
19172
+ }
19168
19173
  function getWorkspaceDir(clawDir, agentName) {
19169
19174
  return import_node_path11.default.join(clawDir, `workspace-${agentName}`);
19170
19175
  }
@@ -19301,6 +19306,11 @@ function detectPackageKind(dir) {
19301
19306
  }
19302
19307
  return "unknown";
19303
19308
  }
19309
+ function getBackupBaseDir(clawDir) {
19310
+ const home = process.env.HOME || "~";
19311
+ const brand = detectClawBrand(clawDir).toLowerCase();
19312
+ return import_node_path11.default.join(home, ".soulhub", "backups", brand);
19313
+ }
19304
19314
  function backupAgentWorkspace(workspaceDir) {
19305
19315
  if (!import_node_fs8.default.existsSync(workspaceDir)) {
19306
19316
  return null;
@@ -19310,7 +19320,7 @@ function backupAgentWorkspace(workspaceDir) {
19310
19320
  return null;
19311
19321
  }
19312
19322
  const clawDir = import_node_path11.default.dirname(workspaceDir);
19313
- const backupBaseDir = import_node_path11.default.join(clawDir, "agentbackup");
19323
+ const backupBaseDir = getBackupBaseDir(clawDir);
19314
19324
  if (!import_node_fs8.default.existsSync(backupBaseDir)) {
19315
19325
  import_node_fs8.default.mkdirSync(backupBaseDir, { recursive: true });
19316
19326
  }
@@ -19333,7 +19343,7 @@ function moveBackupAgentWorkspace(workspaceDir) {
19333
19343
  return null;
19334
19344
  }
19335
19345
  const clawDir = import_node_path11.default.dirname(workspaceDir);
19336
- const backupBaseDir = import_node_path11.default.join(clawDir, "agentbackup");
19346
+ const backupBaseDir = getBackupBaseDir(clawDir);
19337
19347
  if (!import_node_fs8.default.existsSync(backupBaseDir)) {
19338
19348
  import_node_fs8.default.mkdirSync(backupBaseDir, { recursive: true });
19339
19349
  }
@@ -19343,7 +19353,12 @@ function moveBackupAgentWorkspace(workspaceDir) {
19343
19353
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
19344
19354
  backupDir = import_node_path11.default.join(backupBaseDir, `${dirName}-${timestamp2}`);
19345
19355
  }
19346
- import_node_fs8.default.renameSync(workspaceDir, backupDir);
19356
+ try {
19357
+ import_node_fs8.default.renameSync(workspaceDir, backupDir);
19358
+ } catch {
19359
+ import_node_fs8.default.cpSync(workspaceDir, backupDir, { recursive: true });
19360
+ import_node_fs8.default.rmSync(workspaceDir, { recursive: true, force: true });
19361
+ }
19347
19362
  logger.info(`Workspace moved to backup`, { from: workspaceDir, to: backupDir });
19348
19363
  return backupDir;
19349
19364
  }
@@ -19432,6 +19447,106 @@ function commitBackupRecord(record) {
19432
19447
  saveBackupManifest(manifest);
19433
19448
  logger.info(`Backup record saved`, { id: record.id, items: record.items.length, workers: record.installedWorkerIds.length });
19434
19449
  }
19450
+ async function promptSelectRole() {
19451
+ console.log();
19452
+ console.log(" ? Install as:");
19453
+ console.log();
19454
+ console.log(" 1) \u{1F477} Worker agent (\u5B50Agent\uFF0C\u5B89\u88C5\u5230 workspace-<name>/ \u76EE\u5F55)");
19455
+ console.log(" 2) \u{1F451} Main agent (\u4E3BAgent\uFF0C\u5B89\u88C5\u5230 workspace/ \u76EE\u5F55)");
19456
+ console.log();
19457
+ const rl = import_node_readline.default.createInterface({
19458
+ input: process.stdin,
19459
+ output: process.stdout
19460
+ });
19461
+ return new Promise((resolve) => {
19462
+ rl.question(" Please select (1-2) [1]: ", (answer) => {
19463
+ rl.close();
19464
+ const trimmed = answer.trim();
19465
+ if (trimmed === "" || trimmed === "1") {
19466
+ resolve("worker");
19467
+ } else if (trimmed === "2") {
19468
+ resolve("main");
19469
+ } else {
19470
+ console.log(" Invalid selection, operation cancelled.");
19471
+ resolve(null);
19472
+ }
19473
+ });
19474
+ });
19475
+ }
19476
+ async function promptMultiSelectClawDirs() {
19477
+ const dirs = findAllClawDirs();
19478
+ if (dirs.length === 0) {
19479
+ return [];
19480
+ }
19481
+ if (dirs.length === 1) {
19482
+ const brand = detectClawBrand(dirs[0]);
19483
+ console.log();
19484
+ console.log(` \u2714 Detected ${brand}: ${dirs[0]}`);
19485
+ return dirs;
19486
+ }
19487
+ console.log();
19488
+ console.log(" ? Select target Claw installations (multiple allowed):");
19489
+ console.log();
19490
+ dirs.forEach((dir, index) => {
19491
+ const brand = detectClawBrand(dir);
19492
+ console.log(` ${index + 1}) ${brand} ${dir}`);
19493
+ });
19494
+ console.log();
19495
+ const rl = import_node_readline.default.createInterface({
19496
+ input: process.stdin,
19497
+ output: process.stdout
19498
+ });
19499
+ return new Promise((resolve) => {
19500
+ const allNums = dirs.map((_2, i) => String(i + 1)).join(",");
19501
+ rl.question(` Enter numbers separated by comma (e.g. 1,2) [${allNums}]: `, (answer) => {
19502
+ rl.close();
19503
+ const trimmed = answer.trim();
19504
+ if (trimmed === "") {
19505
+ resolve(dirs);
19506
+ return;
19507
+ }
19508
+ const parts = trimmed.split(",").map((s3) => s3.trim());
19509
+ const selected = [];
19510
+ for (const part of parts) {
19511
+ const idx = parseInt(part, 10);
19512
+ if (idx >= 1 && idx <= dirs.length) {
19513
+ const dir = dirs[idx - 1];
19514
+ if (!selected.includes(dir)) {
19515
+ selected.push(dir);
19516
+ }
19517
+ } else {
19518
+ console.log(` Invalid selection: ${part}, operation cancelled.`);
19519
+ resolve([]);
19520
+ return;
19521
+ }
19522
+ }
19523
+ if (selected.length === 0) {
19524
+ console.log(" No claw selected, operation cancelled.");
19525
+ }
19526
+ resolve(selected);
19527
+ });
19528
+ });
19529
+ }
19530
+ async function promptConfirm(message, defaultYes = true) {
19531
+ const rl = import_node_readline.default.createInterface({
19532
+ input: process.stdin,
19533
+ output: process.stdout
19534
+ });
19535
+ const hint = defaultYes ? "Y/n" : "y/N";
19536
+ return new Promise((resolve) => {
19537
+ rl.question(` ${message} (${hint}) `, (answer) => {
19538
+ rl.close();
19539
+ const trimmed = answer.trim().toLowerCase();
19540
+ if (trimmed === "") {
19541
+ resolve(defaultYes);
19542
+ } else if (trimmed === "y" || trimmed === "yes") {
19543
+ resolve(true);
19544
+ } else {
19545
+ resolve(false);
19546
+ }
19547
+ });
19548
+ });
19549
+ }
19435
19550
  var CATEGORY_LABELS = {
19436
19551
  "self-media": "Self Media",
19437
19552
  development: "Development",
@@ -19538,7 +19653,7 @@ function restartOpenClawGateway(clawDir) {
19538
19653
  }
19539
19654
 
19540
19655
  // src/commands/search.ts
19541
- var searchCommand = new Command("search").description("Search for agents 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) => {
19656
+ var searchCommand = new Command("search").description("Search for agents in the SoulHub registry").argument("[query]", "Search query (matches name, description, tags)").option("-c, --category <category>", "Filter by category").option("-n, --limit <number>", "Max results to show", "20").option("--json", "Output results in JSON format").action(async (query, options) => {
19542
19657
  try {
19543
19658
  const index = await fetchIndex();
19544
19659
  let agents = index.agents;
@@ -19556,14 +19671,30 @@ var searchCommand = new Command("search").description("Search for agents in the
19556
19671
  const limit = parseInt(options.limit, 10);
19557
19672
  const shown = agents.slice(0, limit);
19558
19673
  if (shown.length === 0) {
19559
- console.log(source_default.yellow("No agents found matching your query."));
19560
- if (query) {
19561
- console.log(
19562
- source_default.dim(` Try: soulhub search (without query to list all)`)
19563
- );
19674
+ if (options.json) {
19675
+ console.log(JSON.stringify([], null, 2));
19676
+ } else {
19677
+ console.log(source_default.yellow("No agents found matching your query."));
19678
+ if (query) {
19679
+ console.log(
19680
+ source_default.dim(` Try: soulhub search (without query to list all)`)
19681
+ );
19682
+ }
19564
19683
  }
19565
19684
  return;
19566
19685
  }
19686
+ if (options.json) {
19687
+ const jsonOutput = shown.map((a) => ({
19688
+ name: a.name,
19689
+ displayName: a.displayName,
19690
+ version: a.version,
19691
+ description: a.description,
19692
+ category: a.category,
19693
+ tags: a.tags
19694
+ }));
19695
+ console.log(JSON.stringify(jsonOutput, null, 2));
19696
+ return;
19697
+ }
19567
19698
  console.log(
19568
19699
  source_default.bold(`
19569
19700
  Found ${agents.length} agent(s):
@@ -19604,7 +19735,7 @@ var searchCommand = new Command("search").description("Search for agents in the
19604
19735
  });
19605
19736
 
19606
19737
  // src/commands/info.ts
19607
- var infoCommand = new Command("info").description("Show details of an agent (identity, soul, skills, etc.)").argument("<name>", "Agent name").option("--identity", "Show IDENTITY.md content").option("--soul", "Show SOUL.md content").action(async (name, options) => {
19738
+ var infoCommand = new Command("info").description("Show details of an agent (identity, soul, skills, etc.)").argument("<name>", "Agent name").option("--identity", "Show IDENTITY.md content").option("--soul", "Show SOUL.md content").option("--json", "Output results in JSON format").action(async (name, options) => {
19608
19739
  try {
19609
19740
  const index = await fetchIndex();
19610
19741
  const agent = index.agents.find((a) => a.name === name);
@@ -19616,6 +19747,22 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19616
19747
  process.exit(1);
19617
19748
  }
19618
19749
  const category = CATEGORY_LABELS[agent.category] || agent.category;
19750
+ if (options.json) {
19751
+ const jsonOutput = {
19752
+ name: agent.name,
19753
+ displayName: agent.displayName,
19754
+ version: agent.version,
19755
+ description: agent.description,
19756
+ category: agent.category,
19757
+ author: agent.author,
19758
+ tags: agent.tags,
19759
+ minClawVersion: agent.minClawVersion,
19760
+ downloads: agent.downloads,
19761
+ files: agent.files
19762
+ };
19763
+ console.log(JSON.stringify(jsonOutput, null, 2));
19764
+ return;
19765
+ }
19619
19766
  console.log();
19620
19767
  console.log(source_default.bold.cyan(` ${agent.displayName}`));
19621
19768
  console.log(source_default.dim(` ${agent.name} v${agent.version}`));
@@ -19644,7 +19791,7 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19644
19791
  console.log(` ${fileName} ${source_default.dim(`(${sizeStr})`)}`);
19645
19792
  }
19646
19793
  if (options.identity || options.soul) {
19647
- const fs7 = await import("fs");
19794
+ const fs8 = await import("fs");
19648
19795
  const pkgDir = await downloadAgentPackage(name, agent.version);
19649
19796
  try {
19650
19797
  if (options.identity) {
@@ -19652,8 +19799,8 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19652
19799
  console.log(source_default.bold(" \u2500\u2500 IDENTITY.md \u2500\u2500"));
19653
19800
  console.log();
19654
19801
  const identityPath = (await import("path")).default.join(pkgDir, "IDENTITY.md");
19655
- if (fs7.default.existsSync(identityPath)) {
19656
- const content = fs7.default.readFileSync(identityPath, "utf-8");
19802
+ if (fs8.default.existsSync(identityPath)) {
19803
+ const content = fs8.default.readFileSync(identityPath, "utf-8");
19657
19804
  console.log(
19658
19805
  content.split("\n").map((l) => ` ${l}`).join("\n")
19659
19806
  );
@@ -19666,8 +19813,8 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19666
19813
  console.log(source_default.bold(" \u2500\u2500 SOUL.md \u2500\u2500"));
19667
19814
  console.log();
19668
19815
  const soulPath = (await import("path")).default.join(pkgDir, "SOUL.md");
19669
- if (fs7.default.existsSync(soulPath)) {
19670
- const content = fs7.default.readFileSync(soulPath, "utf-8");
19816
+ if (fs8.default.existsSync(soulPath)) {
19817
+ const content = fs8.default.readFileSync(soulPath, "utf-8");
19671
19818
  console.log(
19672
19819
  content.split("\n").map((l) => ` ${l}`).join("\n")
19673
19820
  );
@@ -19676,7 +19823,7 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19676
19823
  }
19677
19824
  }
19678
19825
  } finally {
19679
- fs7.default.rmSync(pkgDir, { recursive: true, force: true });
19826
+ fs8.default.rmSync(pkgDir, { recursive: true, force: true });
19680
19827
  }
19681
19828
  }
19682
19829
  console.log();
@@ -19778,27 +19925,39 @@ function resolveAllClawDirs(clawDir) {
19778
19925
  }
19779
19926
  return findAllClawDirs();
19780
19927
  }
19781
- var installCommand = new Command("install").description("Install an agent or team from the SoulHub registry (default: as worker, installs to all detected claws)").argument("[name]", "Agent or team name to install").option("--from <source>", "Install from a local directory, ZIP file, or URL").option("--main", "Install as main agent").option(
19928
+ var installCommand = new Command("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("-r, --role <role>", "Install role: main or worker (skip role selection prompt)").option(
19782
19929
  "--dir <path>",
19783
19930
  "Target directory (defaults to OpenClaw/LightClaw workspace)"
19784
19931
  ).option(
19785
- "--clawtype <type>",
19932
+ "--claw-type <type>",
19786
19933
  "Specify claw type: OpenClaw or LightClaw (case-insensitive)"
19787
- ).action(async (name, options) => {
19934
+ ).option("-y, --yes", "Skip all confirmation prompts (auto-confirm)").action(async (name, options) => {
19788
19935
  try {
19789
- const asMain = !!options.main;
19936
+ const roleExplicit = !!options.role;
19937
+ const clawExplicit = !!options.clawType || !!options.dir;
19938
+ const skipConfirm = !!options.yes;
19939
+ if (options.role && !["main", "worker"].includes(options.role.toLowerCase())) {
19940
+ console.error(source_default.red(`Invalid role: "${options.role}". Must be "main" or "worker".`));
19941
+ process.exit(1);
19942
+ }
19790
19943
  if (options.from) {
19791
- await installFromSource(options.from, options.dir, options.clawtype, asMain);
19944
+ const asMain = await resolveRole(roleExplicit ? options.role.toLowerCase() === "main" : void 0);
19945
+ if (asMain === null) return;
19946
+ await installFromSource(options.from, options.dir, options.clawType, asMain, clawExplicit, skipConfirm);
19792
19947
  } else if (name) {
19793
- await installFromRegistry(name, options.dir, options.clawtype, asMain);
19948
+ const resolvedRole = roleExplicit ? options.role.toLowerCase() === "main" : void 0;
19949
+ await installFromRegistry(name, options.dir, options.clawType, resolvedRole, clawExplicit, skipConfirm);
19794
19950
  } else {
19795
19951
  console.error(source_default.red("Please specify an agent or team name, or use --from to install from a local source."));
19796
- console.log(source_default.dim(" Examples:"));
19797
- console.log(source_default.dim(" soulhub install writer-wechat # Install as worker to all detected claws"));
19798
- console.log(source_default.dim(" soulhub install writer-wechat --main # Install as main agent"));
19799
- console.log(source_default.dim(" soulhub install dev-squad # Install a team from registry"));
19800
- console.log(source_default.dim(" soulhub install --from ./agent-team/ # Install from local directory"));
19801
- console.log(source_default.dim(" soulhub install writer-wechat --clawtype LightClaw # Install to specific claw"));
19952
+ console.log();
19953
+ console.log(source_default.dim(" Usage:"));
19954
+ console.log(source_default.dim(" soulhub install <name> # Interactive: select role & claw"));
19955
+ console.log(source_default.dim(" soulhub install <name> --role main # As main agent, interactive claw selection"));
19956
+ console.log(source_default.dim(" soulhub install <name> --role worker # As worker agent, interactive claw selection"));
19957
+ console.log(source_default.dim(" soulhub install <name> --claw-type LightClaw # Interactive role, install to specific claw"));
19958
+ console.log(source_default.dim(" soulhub install <name> --role worker --claw-type OpenClaw # Fully non-interactive"));
19959
+ console.log(source_default.dim(" soulhub install <name> --role main --claw-type OpenClaw -y # Non-interactive, skip confirmation"));
19960
+ console.log(source_default.dim(" soulhub install --from ./agent-dir/ # Install from local directory"));
19802
19961
  process.exit(1);
19803
19962
  }
19804
19963
  } catch (error) {
@@ -19810,15 +19969,60 @@ var installCommand = new Command("install").description("Install an agent or tea
19810
19969
  process.exit(1);
19811
19970
  }
19812
19971
  });
19813
- async function installFromRegistry(name, targetDir, clawDir, asMain) {
19972
+ async function resolveRole(explicitMain) {
19973
+ if (explicitMain !== void 0) {
19974
+ return explicitMain;
19975
+ }
19976
+ const role = await promptSelectRole();
19977
+ if (role === null) return null;
19978
+ return role === "main";
19979
+ }
19980
+ async function resolveClawDirsInteractive(clawDir, clawExplicit) {
19981
+ if (clawDir) {
19982
+ return resolveAllClawDirs(clawDir);
19983
+ }
19984
+ if (clawExplicit) {
19985
+ return [];
19986
+ }
19987
+ return promptMultiSelectClawDirs();
19988
+ }
19989
+ async function installFromRegistry(name, targetDir, clawDir, asMain, clawExplicit, skipConfirm = false) {
19814
19990
  const spinner = createSpinner(`Checking registry for ${source_default.cyan(name)}...`).start();
19815
19991
  const index = await fetchIndex();
19816
19992
  const agent = index.agents.find((a) => a.name === name);
19817
19993
  const recipe = index.recipes.find((r) => r.name === name);
19818
19994
  if (agent && !recipe) {
19819
19995
  spinner.stop();
19820
- logger.info(`Installing single agent from registry: ${name}, asMain=${asMain}`);
19821
- await installSingleAgent(name, targetDir, clawDir, asMain);
19996
+ printAgentInfo(agent);
19997
+ const resolvedMain = await resolveRole(asMain);
19998
+ if (resolvedMain === null) return;
19999
+ if (resolvedMain) {
20000
+ console.log();
20001
+ console.log(source_default.yellow(" \u26A0 Installing as main agent will overwrite the current workspace/ content."));
20002
+ console.log(source_default.yellow(" The existing persona (IDENTITY.md, SOUL.md, etc.) will be replaced."));
20003
+ console.log(source_default.yellow(" Memory and conversation history will NOT be affected."));
20004
+ console.log();
20005
+ if (!skipConfirm) {
20006
+ const confirmed = await promptConfirm("Continue?", true);
20007
+ if (!confirmed) {
20008
+ console.log(source_default.dim(" Installation cancelled."));
20009
+ return;
20010
+ }
20011
+ } else {
20012
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
20013
+ }
20014
+ }
20015
+ let resolvedClawDirs;
20016
+ if (!targetDir) {
20017
+ resolvedClawDirs = await resolveClawDirsInteractive(clawDir, clawExplicit);
20018
+ if (resolvedClawDirs.length === 0) {
20019
+ console.log(source_default.red("\n OpenClaw/LightClaw workspace directory not found."));
20020
+ printOpenClawInstallHelp();
20021
+ return;
20022
+ }
20023
+ }
20024
+ logger.info(`Installing single agent from registry: ${name}, asMain=${resolvedMain}`);
20025
+ await installSingleAgent(name, targetDir, clawDir, resolvedMain, resolvedClawDirs);
19822
20026
  } else if (recipe) {
19823
20027
  spinner.stop();
19824
20028
  logger.info(`Installing team recipe from registry: ${name}`);
@@ -19829,12 +20033,25 @@ async function installFromRegistry(name, targetDir, clawDir, asMain) {
19829
20033
  console.log(source_default.dim(" Use 'soulhub search' to find available agents and teams."));
19830
20034
  }
19831
20035
  }
19832
- async function installSingleAgent(name, targetDir, clawDir, asMain = false) {
20036
+ function printAgentInfo(agent) {
20037
+ console.log();
20038
+ console.log(source_default.bold(` \u{1F4E6} ${agent.displayName}`), source_default.dim(`v${agent.version}`));
20039
+ if (agent.description) {
20040
+ console.log(source_default.dim(` ${agent.description}`));
20041
+ }
20042
+ if (agent.category) {
20043
+ console.log(source_default.dim(` Category: ${agent.category}`));
20044
+ }
20045
+ if (agent.tags && agent.tags.length > 0) {
20046
+ console.log(source_default.dim(` Tags: ${agent.tags.join(", ")}`));
20047
+ }
20048
+ }
20049
+ async function installSingleAgent(name, targetDir, clawDir, asMain = false, preResolvedClawDirs) {
19833
20050
  if (targetDir) {
19834
20051
  await installSingleAgentToClaw(name, null, targetDir, asMain);
19835
20052
  return;
19836
20053
  }
19837
- const allClawDirs = resolveAllClawDirs(clawDir);
20054
+ const allClawDirs = preResolvedClawDirs || resolveAllClawDirs(clawDir);
19838
20055
  if (allClawDirs.length === 0) {
19839
20056
  console.log(source_default.red("OpenClaw/LightClaw workspace directory not found."));
19840
20057
  printOpenClawInstallHelp();
@@ -20064,7 +20281,8 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
20064
20281
  await tryRestartGateway(resolvedClawDir);
20065
20282
  }
20066
20283
  }
20067
- async function installFromSource(source, targetDir, clawDir, asMain) {
20284
+ async function installFromSource(source, targetDir, clawDir, asMain, clawExplicit, skipConfirm = false) {
20285
+ if (asMain === null) return;
20068
20286
  const spinner = createSpinner("Analyzing package...").start();
20069
20287
  let packageDir;
20070
20288
  let tempDir = null;
@@ -20118,7 +20336,7 @@ async function installFromSource(source, targetDir, clawDir, asMain) {
20118
20336
  spinner.text = `Detected package type: ${source_default.blue(kind)}`;
20119
20337
  if (kind === "agent") {
20120
20338
  spinner.stop();
20121
- await installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain);
20339
+ await installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain, clawExplicit, skipConfirm);
20122
20340
  } else if (kind === "team") {
20123
20341
  spinner.stop();
20124
20342
  await installTeamFromDir(packageDir, targetDir, clawDir);
@@ -20130,14 +20348,30 @@ async function installFromSource(source, targetDir, clawDir, asMain) {
20130
20348
  import_node_fs9.default.rmSync(tempDir, { recursive: true, force: true });
20131
20349
  }
20132
20350
  }
20133
- async function installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain = false) {
20351
+ async function installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain = false, clawExplicit, skipConfirm = false) {
20134
20352
  const pkg = readSoulHubPackage(packageDir);
20135
20353
  const agentName = pkg?.name || import_node_path12.default.basename(packageDir);
20354
+ if (asMain) {
20355
+ console.log();
20356
+ console.log(source_default.yellow(" \u26A0 Installing as main agent will overwrite the current workspace/ content."));
20357
+ console.log(source_default.yellow(" The existing persona (IDENTITY.md, SOUL.md, etc.) will be replaced."));
20358
+ console.log(source_default.yellow(" Memory and conversation history will NOT be affected."));
20359
+ console.log();
20360
+ if (!skipConfirm) {
20361
+ const confirmed = await promptConfirm("Continue?", true);
20362
+ if (!confirmed) {
20363
+ console.log(source_default.dim(" Installation cancelled."));
20364
+ return;
20365
+ }
20366
+ } else {
20367
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
20368
+ }
20369
+ }
20136
20370
  if (targetDir) {
20137
20371
  await installSingleAgentFromDirToClaw(packageDir, agentName, pkg, null, targetDir, asMain);
20138
20372
  return;
20139
20373
  }
20140
- const allClawDirs = resolveAllClawDirs(clawDir);
20374
+ const allClawDirs = await resolveClawDirsInteractive(clawDir, clawExplicit);
20141
20375
  if (allClawDirs.length === 0) {
20142
20376
  console.log(source_default.red("OpenClaw/LightClaw workspace directory not found."));
20143
20377
  printOpenClawInstallHelp();
@@ -20418,7 +20652,7 @@ async function extractZipToDir(zip, targetDir) {
20418
20652
  }
20419
20653
  function printOpenClawInstallHelp() {
20420
20654
  console.log(source_default.dim(" Please install OpenClaw or LightClaw first, or use one of the following options:"));
20421
- console.log(source_default.dim(" --clawtype <type> Specify claw type: OpenClaw or LightClaw"));
20655
+ console.log(source_default.dim(" --claw-type <type> Specify claw type: OpenClaw or LightClaw"));
20422
20656
  console.log(source_default.dim(" --dir <path> Specify agent target directory directly"));
20423
20657
  console.log(source_default.dim(" OPENCLAW_HOME=<path> Set environment variable (for OpenClaw)"));
20424
20658
  console.log(source_default.dim(" LIGHTCLAW_HOME=<path> Set environment variable (for LightClaw)"));
@@ -20451,17 +20685,31 @@ async function tryRestartGateway(clawDir) {
20451
20685
  }
20452
20686
 
20453
20687
  // src/commands/list.ts
20454
- var listCommand = new Command("list").description("List installed agents").alias("ls").action(async () => {
20688
+ var listCommand = new Command("list").description("List installed agents").alias("ls").option("--json", "Output results in JSON format").action(async (options) => {
20455
20689
  try {
20456
20690
  const config = loadConfig();
20457
20691
  if (config.installed.length === 0) {
20458
- console.log(source_default.yellow("\n No agents installed yet.\n"));
20459
- console.log(
20460
- source_default.dim(" Install one: soulhub install <name>")
20461
- );
20462
- console.log(
20463
- source_default.dim(" Browse all: soulhub search\n")
20464
- );
20692
+ if (options.json) {
20693
+ console.log(JSON.stringify([], null, 2));
20694
+ } else {
20695
+ console.log(source_default.yellow("\n No agents installed yet.\n"));
20696
+ console.log(
20697
+ source_default.dim(" Install one: soulhub install <name>")
20698
+ );
20699
+ console.log(
20700
+ source_default.dim(" Browse all: soulhub search\n")
20701
+ );
20702
+ }
20703
+ return;
20704
+ }
20705
+ if (options.json) {
20706
+ const jsonOutput = config.installed.map((a) => ({
20707
+ name: a.name,
20708
+ version: a.version,
20709
+ installedAt: a.installedAt,
20710
+ workspace: a.workspace
20711
+ }));
20712
+ console.log(JSON.stringify(jsonOutput, null, 2));
20465
20713
  return;
20466
20714
  }
20467
20715
  console.log(
@@ -20519,6 +20767,19 @@ var updateCommand = new Command("update").description("Update installed agents t
20519
20767
  }
20520
20768
  spinner.text = `Updating ${source_default.cyan(installed.name)} (${installed.version} \u2192 ${remote.version})...`;
20521
20769
  const workspaceDir = installed.workspace;
20770
+ const backupDir = backupAgentWorkspace(workspaceDir);
20771
+ if (backupDir) {
20772
+ const backupRecord = createBackupRecord("single-agent", installed.name, workspaceDir);
20773
+ addBackupItem(backupRecord, {
20774
+ originalPath: workspaceDir,
20775
+ backupPath: backupDir,
20776
+ method: "cp",
20777
+ role: "worker",
20778
+ agentId: installed.name
20779
+ });
20780
+ commitBackupRecord(backupRecord);
20781
+ logger.info(`Update backup created for ${installed.name}`, { backupDir });
20782
+ }
20522
20783
  if (!import_node_fs10.default.existsSync(workspaceDir)) {
20523
20784
  import_node_fs10.default.mkdirSync(workspaceDir, { recursive: true });
20524
20785
  }
@@ -20547,20 +20808,109 @@ var updateCommand = new Command("update").description("Update installed agents t
20547
20808
  }
20548
20809
  });
20549
20810
 
20550
- // src/commands/rollback.ts
20811
+ // src/commands/uninstall.ts
20551
20812
  var import_node_fs11 = __toESM(require("fs"), 1);
20813
+ var uninstallCommand = new Command("uninstall").description("Uninstall an agent template").alias("rm").argument("<name>", "Agent name to uninstall").option("--keep-files", "Remove from registry but keep workspace files").option("-y, --yes", "Skip all confirmation prompts (auto-confirm)").action(async (name, options) => {
20814
+ try {
20815
+ const config = loadConfig();
20816
+ const installed = config.installed.find((a) => a.name === name);
20817
+ if (!installed) {
20818
+ console.error(
20819
+ source_default.red(`
20820
+ Agent "${name}" is not installed.
20821
+ `)
20822
+ );
20823
+ console.log(
20824
+ source_default.dim(" Use 'soulhub list' to see installed agents.")
20825
+ );
20826
+ process.exit(1);
20827
+ }
20828
+ const spinner = createSpinner(
20829
+ `Uninstalling ${source_default.cyan(name)}...`
20830
+ ).start();
20831
+ const manifest = loadBackupManifest();
20832
+ const relatedRecords = manifest.records.filter((r) => r.packageName === name);
20833
+ if (relatedRecords.length > 0) {
20834
+ spinner.stop();
20835
+ console.log(
20836
+ source_default.yellow(`
20837
+ \u26A0 Found ${relatedRecords.length} backup record(s) for "${name}".`)
20838
+ );
20839
+ console.log(
20840
+ source_default.yellow(` Uninstalling will also delete all related backup files.`)
20841
+ );
20842
+ console.log(
20843
+ source_default.yellow(` After deletion, you will NOT be able to rollback this agent.
20844
+ `)
20845
+ );
20846
+ if (!options.yes) {
20847
+ const confirmed = await promptConfirm("Proceed with uninstall?");
20848
+ if (!confirmed) {
20849
+ console.log(source_default.dim("\n Uninstall cancelled.\n"));
20850
+ return;
20851
+ }
20852
+ } else {
20853
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
20854
+ }
20855
+ spinner.start(`Uninstalling ${source_default.cyan(name)}...`);
20856
+ }
20857
+ if (!options.keepFiles && import_node_fs11.default.existsSync(installed.workspace)) {
20858
+ import_node_fs11.default.rmSync(installed.workspace, { recursive: true, force: true });
20859
+ spinner.text = `Removed workspace: ${installed.workspace}`;
20860
+ }
20861
+ removeInstallRecord(name);
20862
+ if (relatedRecords.length > 0) {
20863
+ for (const record of relatedRecords) {
20864
+ for (const item of record.items) {
20865
+ if (import_node_fs11.default.existsSync(item.backupPath)) {
20866
+ import_node_fs11.default.rmSync(item.backupPath, { recursive: true, force: true });
20867
+ logger.info(`Cleaned backup file for uninstalled agent`, { backupPath: item.backupPath });
20868
+ }
20869
+ }
20870
+ }
20871
+ manifest.records = manifest.records.filter((r) => r.packageName !== name);
20872
+ saveBackupManifest(manifest);
20873
+ spinner.text = `Cleaned ${relatedRecords.length} backup record(s)`;
20874
+ logger.info(`Cleaned ${relatedRecords.length} backup record(s) for ${name}`);
20875
+ }
20876
+ logger.info(`Agent uninstalled: ${name}`, { workspace: installed.workspace, keepFiles: !!options.keepFiles });
20877
+ spinner.succeed(
20878
+ `${source_default.cyan.bold(name)} uninstalled.`
20879
+ );
20880
+ if (options.keepFiles) {
20881
+ console.log(
20882
+ source_default.dim(` Files kept at: ${installed.workspace}`)
20883
+ );
20884
+ }
20885
+ console.log();
20886
+ } catch (error) {
20887
+ logger.errorObj("Uninstall command failed", error);
20888
+ console.error(
20889
+ source_default.red(`Error: ${error instanceof Error ? error.message : error}`)
20890
+ );
20891
+ console.error(source_default.dim(` See logs: ${logger.getTodayLogFile()}`));
20892
+ process.exit(1);
20893
+ }
20894
+ });
20895
+
20896
+ // src/commands/rollback.ts
20897
+ var import_node_fs12 = __toESM(require("fs"), 1);
20552
20898
  var import_node_path13 = __toESM(require("path"), 1);
20553
- var rollbackCommand = new Command("rollback").description("Rollback to a previous agent installation state").option("--list", "List available rollback records").option("--id <id>", "Rollback to a specific backup record by ID").option(
20554
- "--clawtype <type>",
20899
+ var import_node_readline2 = __toESM(require("readline"), 1);
20900
+ var rollbackCommand = new Command("rollback").description("Rollback to a previous agent installation state").option("--list", "List available rollback records").option("--id <id>", "Rollback to a specific backup record by ID").option("--last <n>", "Rollback the Nth most recent installation (1 = latest, 2 = second latest, etc.)", parseInt).option(
20901
+ "--claw-type <type>",
20555
20902
  "Specify claw type: OpenClaw or LightClaw (case-insensitive)"
20556
- ).action(async (options) => {
20903
+ ).option("-y, --yes", "Skip all confirmation prompts (auto-confirm)").action(async (options) => {
20557
20904
  try {
20905
+ const skipConfirm = !!options.yes;
20558
20906
  if (options.list) {
20559
20907
  listBackupRecords();
20560
20908
  } else if (options.id) {
20561
- await performRollback(options.id, options.clawtype);
20909
+ await performRollback(options.id, options.clawType, skipConfirm);
20910
+ } else if (options.last) {
20911
+ await performRollbackByIndex(options.last, options.clawType, skipConfirm);
20562
20912
  } else {
20563
- await performRollback(void 0, options.clawtype);
20913
+ await interactiveRollback(options.clawType);
20564
20914
  }
20565
20915
  } catch (error) {
20566
20916
  logger.errorObj("Rollback command failed", error);
@@ -20579,53 +20929,192 @@ function listBackupRecords() {
20579
20929
  return;
20580
20930
  }
20581
20931
  console.log(source_default.bold("\nAvailable rollback records:\n"));
20932
+ printRecordTable(manifest.records);
20933
+ console.log();
20934
+ console.log(source_default.dim(" Usage:"));
20935
+ console.log(source_default.dim(" soulhub rollback # Interactive: select a record to rollback"));
20936
+ console.log(source_default.dim(" soulhub rollback --last 1 # Rollback the latest installation"));
20937
+ console.log(source_default.dim(" soulhub rollback --last 2 # Rollback the 2nd latest installation"));
20938
+ console.log(source_default.dim(" soulhub rollback --id <id> # Rollback to a specific record by ID"));
20939
+ console.log();
20940
+ }
20941
+ function printRecordTable(records) {
20582
20942
  console.log(
20583
20943
  source_default.dim(
20584
- ` ${"ID".padEnd(20)} ${"Type".padEnd(20)} ${"Package".padEnd(20)} ${"Date".padEnd(22)} Items`
20944
+ ` ${"#".padEnd(4)} ${"ID".padEnd(20)} ${"Type".padEnd(20)} ${"Package".padEnd(20)} ${"Claw".padEnd(14)} ${"Date".padEnd(22)} Items`
20585
20945
  )
20586
20946
  );
20587
- console.log(source_default.dim(" " + "\u2500".repeat(90)));
20588
- for (const record of manifest.records) {
20947
+ console.log(source_default.dim(" " + "\u2500".repeat(108)));
20948
+ records.forEach((record, index) => {
20589
20949
  const date = new Date(record.createdAt).toLocaleString();
20590
20950
  const typeLabel = formatInstallType(record.installType);
20591
20951
  const itemCount = record.items.length;
20952
+ const clawBrand = detectClawBrandFromDir(record.clawDir);
20592
20953
  console.log(
20593
- ` ${source_default.cyan(record.id.padEnd(20))} ${typeLabel.padEnd(20)} ${source_default.white(record.packageName.padEnd(20))} ${source_default.dim(date.padEnd(22))} ${itemCount} backup(s)`
20954
+ ` ${source_default.yellow(String(index + 1).padEnd(4))} ${source_default.cyan(record.id.padEnd(20))} ${typeLabel.padEnd(20)} ${source_default.white(record.packageName.padEnd(20))} ${source_default.dim(clawBrand.padEnd(14))} ${source_default.dim(date.padEnd(22))} ${itemCount} backup(s)`
20594
20955
  );
20956
+ });
20957
+ }
20958
+ async function interactiveRollback(clawDir) {
20959
+ const manifest = loadBackupManifest();
20960
+ if (manifest.records.length === 0) {
20961
+ console.log(source_default.yellow("No backup records found. Nothing to rollback."));
20962
+ console.log(source_default.dim(" Backup records are created automatically when you install agents."));
20963
+ return;
20595
20964
  }
20965
+ console.log(source_default.bold("\n Select a record to rollback:\n"));
20966
+ printRecordTable(manifest.records);
20596
20967
  console.log();
20597
- console.log(source_default.dim(" Usage:"));
20598
- console.log(source_default.dim(" soulhub rollback # Rollback last installation"));
20599
- console.log(source_default.dim(" soulhub rollback --id <id> # Rollback to a specific record"));
20968
+ const rl = import_node_readline2.default.createInterface({
20969
+ input: process.stdin,
20970
+ output: process.stdout
20971
+ });
20972
+ const selected = await new Promise((resolve) => {
20973
+ rl.question(` Enter number to rollback (1-${manifest.records.length}), or 'q' to cancel: `, (answer) => {
20974
+ rl.close();
20975
+ const trimmed = answer.trim().toLowerCase();
20976
+ if (trimmed === "q" || trimmed === "quit" || trimmed === "") {
20977
+ resolve(null);
20978
+ return;
20979
+ }
20980
+ const idx = parseInt(trimmed, 10);
20981
+ if (idx >= 1 && idx <= manifest.records.length) {
20982
+ resolve(idx);
20983
+ } else {
20984
+ resolve(null);
20985
+ }
20986
+ });
20987
+ });
20988
+ if (selected === null) {
20989
+ console.log(source_default.dim(" Rollback cancelled."));
20990
+ return;
20991
+ }
20992
+ const record = manifest.records[selected - 1];
20600
20993
  console.log();
20994
+ console.log(source_default.dim(` Selected: ${source_default.cyan(record.id)} (${record.packageName})`));
20995
+ printRollbackDetails(record);
20996
+ const confirmed = await promptConfirmRollback();
20997
+ if (!confirmed) {
20998
+ console.log(source_default.dim(" Rollback cancelled."));
20999
+ return;
21000
+ }
21001
+ await executeRollback(record, clawDir);
20601
21002
  }
20602
- async function performRollback(recordId, clawDir) {
21003
+ async function performRollbackByIndex(n, clawDir, skipConfirm = false) {
20603
21004
  const manifest = loadBackupManifest();
20604
21005
  if (manifest.records.length === 0) {
20605
21006
  console.log(source_default.yellow("No backup records found. Nothing to rollback."));
20606
21007
  return;
20607
21008
  }
20608
- let record;
20609
- if (recordId) {
20610
- record = manifest.records.find((r) => r.id === recordId);
20611
- if (!record) {
20612
- console.error(source_default.red(`Backup record "${recordId}" not found.`));
20613
- console.log(source_default.dim(" Use 'soulhub rollback --list' to see available records."));
21009
+ if (n < 1 || n > manifest.records.length) {
21010
+ console.error(source_default.red(`Invalid index: ${n}. Available range: 1-${manifest.records.length}`));
21011
+ console.log(source_default.dim(" Use 'soulhub rollback --list' to see all available records."));
21012
+ return;
21013
+ }
21014
+ const record = manifest.records[n - 1];
21015
+ console.log(
21016
+ source_default.dim(
21017
+ `
21018
+ Rolling back #${n}: ${source_default.cyan(record.id)} (${record.packageName})`
21019
+ )
21020
+ );
21021
+ printRollbackDetails(record);
21022
+ if (!skipConfirm) {
21023
+ const rl2 = import_node_readline2.default.createInterface({
21024
+ input: process.stdin,
21025
+ output: process.stdout
21026
+ });
21027
+ const confirmed = await new Promise((resolve) => {
21028
+ rl2.question(` ${source_default.yellow("\u26A0")} Proceed with rollback? (Y/n) `, (answer) => {
21029
+ rl2.close();
21030
+ const trimmed = answer.trim().toLowerCase();
21031
+ resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
21032
+ });
21033
+ });
21034
+ if (!confirmed) {
21035
+ console.log(source_default.dim(" Rollback cancelled."));
20614
21036
  return;
20615
21037
  }
20616
21038
  } else {
20617
- record = manifest.records[0];
20618
- console.log(
20619
- source_default.dim(
20620
- `Rolling back last installation: ${source_default.cyan(record.id)} (${record.packageName})`
20621
- )
20622
- );
21039
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
21040
+ }
21041
+ await executeRollback(record, clawDir);
21042
+ }
21043
+ async function performRollback(recordId, clawDir, skipConfirm = false) {
21044
+ const manifest = loadBackupManifest();
21045
+ if (manifest.records.length === 0) {
21046
+ console.log(source_default.yellow("No backup records found. Nothing to rollback."));
21047
+ return;
20623
21048
  }
21049
+ const record = manifest.records.find((r) => r.id === recordId);
21050
+ if (!record) {
21051
+ console.error(source_default.red(`Backup record "${recordId}" not found.`));
21052
+ console.log(source_default.dim(" Use 'soulhub rollback --list' to see available records."));
21053
+ return;
21054
+ }
21055
+ console.log(
21056
+ source_default.dim(
21057
+ `
21058
+ Rolling back: ${source_default.cyan(record.id)} (${record.packageName})`
21059
+ )
21060
+ );
21061
+ printRollbackDetails(record);
21062
+ if (!skipConfirm) {
21063
+ const confirmed = await promptConfirmRollback();
21064
+ if (!confirmed) {
21065
+ console.log(source_default.dim(" Rollback cancelled."));
21066
+ return;
21067
+ }
21068
+ } else {
21069
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
21070
+ }
21071
+ await executeRollback(record, clawDir);
21072
+ }
21073
+ function printRollbackDetails(record) {
21074
+ console.log();
21075
+ console.log(source_default.dim(" Rollback details:"));
21076
+ console.log(source_default.dim(` Package: ${record.packageName}`));
21077
+ console.log(source_default.dim(` Type: ${formatInstallType(record.installType)}`));
21078
+ console.log(source_default.dim(` Claw dir: ${record.clawDir}`));
21079
+ console.log(source_default.dim(` Date: ${new Date(record.createdAt).toLocaleString()}`));
21080
+ if (record.items.length > 0) {
21081
+ console.log(source_default.dim(` Backups to restore:`));
21082
+ for (const item of record.items) {
21083
+ const methodLabel = item.method === "mv" ? "move back" : "copy back";
21084
+ console.log(source_default.dim(` - ${item.agentId} (${item.role}, ${methodLabel})`));
21085
+ }
21086
+ }
21087
+ if (record.installedWorkerIds.length > 0) {
21088
+ console.log(source_default.dim(` Workers to remove: ${record.installedWorkerIds.join(", ")}`));
21089
+ }
21090
+ if (record.installedMainAgent) {
21091
+ console.log(source_default.dim(` Main agent to revert: ${record.installedMainAgent}`));
21092
+ }
21093
+ console.log();
21094
+ }
21095
+ async function promptConfirmRollback() {
21096
+ const rl = import_node_readline2.default.createInterface({
21097
+ input: process.stdin,
21098
+ output: process.stdout
21099
+ });
21100
+ return new Promise((resolve) => {
21101
+ rl.question(` ${source_default.yellow("\u26A0")} Proceed with rollback? (Y/n) `, (answer) => {
21102
+ rl.close();
21103
+ const trimmed = answer.trim().toLowerCase();
21104
+ if (trimmed === "" || trimmed === "y" || trimmed === "yes") {
21105
+ resolve(true);
21106
+ } else {
21107
+ resolve(false);
21108
+ }
21109
+ });
21110
+ });
21111
+ }
21112
+ async function executeRollback(record, clawDir) {
20624
21113
  const spinner = createSpinner(
20625
21114
  `Rolling back ${source_default.cyan(record.packageName)}...`
20626
21115
  ).start();
20627
- const resolvedClawDir = clawDir ? findOpenClawDir(clawDir) || record.clawDir : await promptSelectClawDir() || record.clawDir;
20628
- if (!resolvedClawDir || !import_node_fs11.default.existsSync(resolvedClawDir)) {
21116
+ const resolvedClawDir = clawDir ? findOpenClawDir(clawDir) || record.clawDir : record.clawDir;
21117
+ if (!resolvedClawDir || !import_node_fs12.default.existsSync(resolvedClawDir)) {
20629
21118
  spinner.fail(`OpenClaw/LightClaw directory not found: ${record.clawDir}`);
20630
21119
  return;
20631
21120
  }
@@ -20645,57 +21134,63 @@ async function performRollback(recordId, clawDir) {
20645
21134
  spinner.text = "Removing installed workers...";
20646
21135
  for (const workerId of record.installedWorkerIds) {
20647
21136
  const workerDir = getWorkspaceDir(resolvedClawDir, workerId);
20648
- if (import_node_fs11.default.existsSync(workerDir)) {
20649
- import_node_fs11.default.rmSync(workerDir, { recursive: true, force: true });
21137
+ if (import_node_fs12.default.existsSync(workerDir)) {
21138
+ import_node_fs12.default.rmSync(workerDir, { recursive: true, force: true });
20650
21139
  logger.info(`Removed installed worker directory`, { workerId, dir: workerDir });
20651
21140
  }
20652
21141
  const agentConfigDir = import_node_path13.default.join(resolvedClawDir, "agents", workerId);
20653
- if (import_node_fs11.default.existsSync(agentConfigDir)) {
20654
- import_node_fs11.default.rmSync(agentConfigDir, { recursive: true, force: true });
21142
+ if (import_node_fs12.default.existsSync(agentConfigDir)) {
21143
+ import_node_fs12.default.rmSync(agentConfigDir, { recursive: true, force: true });
20655
21144
  }
20656
21145
  }
20657
21146
  }
20658
21147
  if (record.installedMainAgent) {
20659
21148
  const mainWorkspace = getMainWorkspaceDir(resolvedClawDir);
20660
21149
  const hasMainBackup = record.items.some((item) => item.role === "main");
20661
- if (hasMainBackup && import_node_fs11.default.existsSync(mainWorkspace)) {
21150
+ if (hasMainBackup && import_node_fs12.default.existsSync(mainWorkspace)) {
20662
21151
  spinner.text = "Cleaning current main workspace...";
20663
- const entries = import_node_fs11.default.readdirSync(mainWorkspace);
21152
+ const entries = import_node_fs12.default.readdirSync(mainWorkspace);
20664
21153
  for (const entry of entries) {
20665
- import_node_fs11.default.rmSync(import_node_path13.default.join(mainWorkspace, entry), { recursive: true, force: true });
21154
+ import_node_fs12.default.rmSync(import_node_path13.default.join(mainWorkspace, entry), { recursive: true, force: true });
20666
21155
  }
20667
21156
  }
20668
21157
  }
20669
21158
  let restoredCount = 0;
20670
21159
  for (const item of record.items) {
20671
- if (!import_node_fs11.default.existsSync(item.backupPath)) {
21160
+ if (!import_node_fs12.default.existsSync(item.backupPath)) {
20672
21161
  logger.warn(`Backup path not found, skipping`, { backupPath: item.backupPath });
20673
21162
  console.log(source_default.yellow(` \u26A0 Backup not found: ${item.backupPath}, skipping...`));
20674
21163
  continue;
20675
21164
  }
20676
21165
  spinner.text = `Restoring ${source_default.cyan(item.agentId)} (${item.role})...`;
20677
21166
  if (item.method === "mv") {
20678
- if (import_node_fs11.default.existsSync(item.originalPath)) {
20679
- import_node_fs11.default.rmSync(item.originalPath, { recursive: true, force: true });
21167
+ if (import_node_fs12.default.existsSync(item.originalPath)) {
21168
+ import_node_fs12.default.rmSync(item.originalPath, { recursive: true, force: true });
21169
+ }
21170
+ import_node_fs12.default.mkdirSync(import_node_path13.default.dirname(item.originalPath), { recursive: true });
21171
+ try {
21172
+ import_node_fs12.default.renameSync(item.backupPath, item.originalPath);
21173
+ } catch {
21174
+ import_node_fs12.default.cpSync(item.backupPath, item.originalPath, { recursive: true });
21175
+ import_node_fs12.default.rmSync(item.backupPath, { recursive: true, force: true });
20680
21176
  }
20681
- import_node_fs11.default.mkdirSync(import_node_path13.default.dirname(item.originalPath), { recursive: true });
20682
- import_node_fs11.default.renameSync(item.backupPath, item.originalPath);
20683
21177
  logger.info(`Restored (mv back)`, { from: item.backupPath, to: item.originalPath });
20684
21178
  } else {
20685
- if (import_node_fs11.default.existsSync(item.originalPath)) {
20686
- const entries = import_node_fs11.default.readdirSync(item.originalPath);
21179
+ if (import_node_fs12.default.existsSync(item.originalPath)) {
21180
+ const entries = import_node_fs12.default.readdirSync(item.originalPath);
20687
21181
  for (const entry of entries) {
20688
- import_node_fs11.default.rmSync(import_node_path13.default.join(item.originalPath, entry), { recursive: true, force: true });
21182
+ import_node_fs12.default.rmSync(import_node_path13.default.join(item.originalPath, entry), { recursive: true, force: true });
20689
21183
  }
20690
21184
  } else {
20691
- import_node_fs11.default.mkdirSync(item.originalPath, { recursive: true });
21185
+ import_node_fs12.default.mkdirSync(item.originalPath, { recursive: true });
20692
21186
  }
20693
- import_node_fs11.default.cpSync(item.backupPath, item.originalPath, { recursive: true });
20694
- import_node_fs11.default.rmSync(item.backupPath, { recursive: true, force: true });
21187
+ import_node_fs12.default.cpSync(item.backupPath, item.originalPath, { recursive: true });
21188
+ import_node_fs12.default.rmSync(item.backupPath, { recursive: true, force: true });
20695
21189
  logger.info(`Restored (cp back)`, { from: item.backupPath, to: item.originalPath });
20696
21190
  }
20697
21191
  restoredCount++;
20698
21192
  }
21193
+ const manifest = loadBackupManifest();
20699
21194
  manifest.records = manifest.records.filter((r) => r.id !== record.id);
20700
21195
  saveBackupManifest(manifest);
20701
21196
  spinner.succeed(
@@ -20729,16 +21224,23 @@ function formatInstallType(type2) {
20729
21224
  return type2;
20730
21225
  }
20731
21226
  }
21227
+ function detectClawBrandFromDir(clawDir) {
21228
+ const dirName = import_node_path13.default.basename(clawDir).toLowerCase();
21229
+ if (dirName.includes("lightclaw")) {
21230
+ return "LightClaw";
21231
+ }
21232
+ return "OpenClaw";
21233
+ }
20732
21234
 
20733
21235
  // src/index.ts
20734
21236
  var program2 = new Command();
20735
- program2.name("soulhub").description("SoulHub CLI - Discover, install and manage AI agent souls").version("1.0.21").option("--verbose", "Enable verbose debug logging").hook("preAction", () => {
21237
+ program2.name("soulhub").description("SoulHub CLI - Discover, install and manage AI agent souls").version("1.0.22").option("--verbose", "Enable verbose debug logging").hook("preAction", () => {
20736
21238
  const opts = program2.opts();
20737
21239
  const verbose = opts.verbose || process.env.SOULHUB_DEBUG === "1";
20738
21240
  logger.init(verbose);
20739
21241
  logger.info("CLI started", {
20740
21242
  args: process.argv.slice(2),
20741
- version: "1.0.21",
21243
+ version: "1.0.22",
20742
21244
  node: process.version
20743
21245
  });
20744
21246
  });
@@ -20747,6 +21249,7 @@ program2.addCommand(infoCommand);
20747
21249
  program2.addCommand(installCommand);
20748
21250
  program2.addCommand(listCommand);
20749
21251
  program2.addCommand(updateCommand);
21252
+ program2.addCommand(uninstallCommand);
20750
21253
  program2.addCommand(rollbackCommand);
20751
21254
  program2.parse();
20752
21255
  /*! Bundled license information:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulhubcli",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "SoulHub CLI - Install and manage AI agent persona templates for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {