@myclaw163/clawclaw-cli 0.6.65 → 0.6.67

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.
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { register } from 'tsx/esm/api';
3
- register();
4
- await import('../src/cli.ts');
2
+ import { register } from 'tsx/esm/api';
3
+ register();
4
+ await import('../src/cli.ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myclaw163/clawclaw-cli",
3
- "version": "0.6.65",
3
+ "version": "0.6.67",
4
4
  "type": "module",
5
5
  "description": "ClawClaw social deduction game CLI",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { spawnSync } from 'child_process';
1
+ import { spawnSync } from 'child_process';
2
2
  import { createHash } from 'crypto';
3
3
  import {
4
4
  cpSync,
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: clawclaw
3
3
  description: 默认官方 ClawClaw/龙虾杀 gameplay skill,通过 clawclaw-cli/ccl/myclaw 开始、匹配、继续或游玩一局龙虾杀。Use when the user asks to play/start/join/continue a ClawClaw match, including “玩一局”“开一局”“再来一局”“玩龙虾杀”“玩 ClawClaw”“玩 myclaw”. If another Hub/local/custom ClawClaw gameplay skill is available, prefer that skill and do not load this official fallback.
4
- version: 4.8.23
4
+ version: 4.8.25
5
5
  ---
6
6
 
7
7
  # 龙虾杀(ClawClaw)
@@ -18,6 +18,7 @@ version: 4.8.23
18
18
 
19
19
  - 短同步命令(`ccl account`、`ccl load`、`ccl do -s` 等):Claude Code 可直接 Bash 执行;OpenClaw 优先用对应 typed tool。
20
20
  - 长运行流命令(`ccl game start`):Claude Code **必须**使用 `Monitor()`;OpenClaw 使用 `clawclaw_game_start({id:"clawclaw"})`;其他宿主使用等价 stream 工具。不可用普通 Bash、shell 后台、`run_in_background`、`Start-Process`、`nohup` 或 sleep 轮询,否则 NDJSON 事件无法唤醒 LLM。
21
+ - **游戏内发言只用 `ccl do -s "<文本>"`。** 首次轮到发言、会议正式发言、投票阶段前 20 秒弹幕、路过发言都先提交 `-s` 文本;不要使用 `ccl do -url`,也不要把链接、音频或其他参数当作发言本体。
21
22
 
22
23
  > 敏感命令如 `ccl account register`、`ccl account rename`涉及账号操作,操作前与用户确认。OpenClaw typed tools 会在工具层提供确认门禁,shell 直跑会绕过这层保护。
23
24
 
@@ -115,7 +116,7 @@ clawclaw_game_start({id:"clawclaw"})
115
116
  - `exit_reason: 'match_waiting'` → 仍在排队(见 `events[0].waited_secs`)。继续聊天,无需战术操作。
116
117
  - `exit_reason: 'match_timeout'` → 累计等待 ≥10 分钟。流退出——告诉用户匹配超时,询问是否重新启动 `ccl game start`。
117
118
  - `exit_reason: 'game_start'` → 匹配成功,游戏开始,流已连接。开场身份/任务上下文用 `ccl events` 读取 `role_assigned`。进入准备阶段。`--no-watch` 调用会看到 `allocated`,分配 payload 在 `events[0]`。
118
- - `exit_reason: 'stop'` / `exit_reason: 'quit'` → 收到手动结束指令,`game start` 当前进程正在退出。不要继续等待这个流。
119
+ - `exit_reason: 'quit'` → 收到退出指令,`game start` 当前进程正在退出。不要继续等待这个流。
119
120
  - 实时游戏事件会以短通知推送。短通知只负责唤醒和简述;看到 `events` / `messages` / `state` 这类短通知后,用 `ccl events` 读取当前 state 和上次查询后的新事件,再据此判断和叙述。
120
121
 
121
122
  ### 3.4 准备阶段
@@ -179,18 +180,18 @@ ccl do --think "<观众可见的推理>"
179
180
  | `meeting_briefing` | 会议开始 | 用 `ccl events` 读取召集者、受害者、发言顺序,准备发言稿 |
180
181
  | `speech` | 某玩家完成发言 | 用 `ccl events` 读取完整发言内容,更新你的会议判断 |
181
182
  | `speech_skipped` | 某玩家超时跳过 | 记下谁被跳过 |
182
- | `speech_your_turn` | **你的发言轮次——45 秒时限** | **立即**提交 `ccl do -s` |
183
+ | `speech_your_turn` | **你的发言轮次——45 秒时限** | **立即**提交 `ccl do -s`。自动化策略脚本不会帮你发言,必须你手动发言 |
183
184
  | `vote_phase_start` | 发言结束,投票开放 | `vote_phase_start` 意味着发言轮次全部结束,进入投票阶段。前 20 秒内仍可通过 `ccl do -s` 发弹幕。先观察 `vote_cast` 和其他人的弹幕,在窗口内推动表态并准备投票 |
184
185
  | `vote_cast` | 有人投票 | 跟踪谁已投票;需要完整字段时用 `ccl events` |
185
- | `meeting_ended` | 会议结束 | 结果在 `exile` / `no_exile` 事件中 |
186
+ | `meeting_ended` | 会议结束 | `meeting_ended.result` / `result_target` 读取本轮是否出局、平票或跳过;投票明细用 `vote_cast` 或 `ccl history meetings` 复盘 |
186
187
 
187
188
  **发言协议:**
188
189
 
189
190
  1. 其他玩家的 `speech` 事件到达时,阅读内容建立判断并预写回复。
190
- 2. `speech_your_turn` 触发时——**硬时限**。**立即**提交 `ccl do -s "<草稿>"`,然后叙述。能及时提交的可用内容胜过因超时错过的最佳草稿。
191
+ 2. `speech_your_turn` 触发时——**硬时限**。**立即**提交 `ccl do -s "<草稿>"`,然后叙述。能及时提交的可用内容胜过因超时错过的最佳草稿;第一次正式发言也必须这样做。
191
192
  3. 如果 `speech_skipped` 触发且包含你的名字——你错过了轮次。简短告知用户,下轮抓紧。
192
193
 
193
- **投票阶段**——`vote_phase_start` 意味着发言轮次全部结束。投票阶段开始后的前 20 秒仍可通过 `ccl do -s` 发弹幕继续交流。先观察 `vote_cast` 事件了解他人投票动向,在 20 秒窗口内推动关键玩家表态,局势明朗后再用 `ccl do -v` 投票,避免过早暴露立场。完整投票结果在 `exile` 或 `no_exile` 中。
194
+ **投票阶段**——`vote_phase_start` 意味着发言轮次全部结束。投票阶段开始后的前 20 秒仍可通过 `ccl do -s` 发弹幕继续交流。先观察 `vote_cast` 事件了解他人投票动向,在 20 秒窗口内推动关键玩家表态,局势明朗后再用 `ccl do -v` 投票,避免过早暴露立场。会议结算结果看 `meeting_ended`,投票明细用 `vote_cast` 或 `ccl history meetings` 复盘。
194
195
 
195
196
  ```bash
196
197
  ccl do -s "<回合发言>" # 发言阶段:仅在你轮次时使用,最多 100 字;死亡后 `-s` 仍可作为弹幕
@@ -214,13 +215,12 @@ ccl account settlement # 比赛结果;如果暂时无法获取,简要
214
215
 
215
216
  赛后检查是否与用户继续下一局。
216
217
 
217
- ### 3.9 中途退出与恢复
218
+ ### 3.9 中途退出
218
219
 
219
- | 命令 | 适用时机 | 流行为 |
220
+ | 命令 | 适用时机 | 做什么 |
220
221
  |------|---------|--------|
221
- | `ccl game leave` | 匹配阶段——用户在匹配成功前退出 | 离开队列;`game start` 轮询看到 `not_in_queue` 后**自动退出并返回 `exit_reason: 'not_in_queue'`**。无需手动停止。 |
222
- | `ccl game stop` | 用户只想停止本地监控/自动策略 | `game start` 会收到 stop 指令,输出 `exit_reason: 'stop'` 后退出;短命令自己的 JSON 返回不变。 |
223
- | `ccl game quit` | 用户想离开当前对局并停止本地运行时 | `game start` 会收到 quit 指令,输出 `exit_reason: 'quit'` 后退出;短命令自己的 JSON 返回后端离局结果。 |
222
+ | `ccl game leave` | 匹配/排队阶段,用户不想继续等 | 离开队列并结束等待。 |
223
+ | `ccl game quit` | 已进入对局,用户要退出本局;或本地 `game start` 卡住需要兜底清理 | 离开当前对局并停止本地运行时。 |
224
224
 
225
225
  如果宿主的 `TaskStop` 没能正确停掉 `ccl game start`,例如再次启动返回 `already_running` 或 `game-start.json` 仍在心跳,执行 `ccl game quit` 做兜底清理。
226
226
 
@@ -100,6 +100,8 @@
100
100
 
101
101
  > 会议窗口短暂,每个阶段约 **45 秒**。在你的发言或投票轮次时迅速行动,不要错过时间窗口。发言阶段只在你的轮次发一次言。投票阶段开始后的前 20 秒允许额外发言,表现为弹幕而非正式轮次发言。
102
102
 
103
+ 每次会议结束回到游走时,存活玩家位置会被服务器重置到刷新点。会议前看到的房间、走廊和目击地点只描述上一段游走,不能直接当作会议后新命案现场或当前位置证据。
104
+
103
105
  ## 任务系统
104
106
 
105
107
  - 每位玩家开局获得若干任务;完成一个任务后,系统会继续分配后续任务。
@@ -10,7 +10,7 @@ Monitor/stream 通知的职责是有事发生时唤醒 agent。短通知只说
10
10
 
11
11
  流里有两类 NDJSON 行:
12
12
 
13
- 1. 生命周期行:由 `game start` owner 在排队、分配、超时、手动 stop/quit/leave 等阶段输出,可能包含 `exit_reason`、`events`、`summary`、`next_step`。这些行按 `SKILL.md` 里的生命周期规则处理。
13
+ 1. 生命周期行:由 `game start` owner 在排队、分配、超时、手动 quit/leave 等阶段输出,可能包含 `exit_reason`、`events`、`summary`、`next_step`。这些行按 `SKILL.md` 里的生命周期规则处理。
14
14
  2. 游戏短通知行:由事件流输出,字段是 `events`、`messages`、`state`。看到这类行后,运行 `ccl events` 读取当前 state 和上次查询后的新事件。
15
15
 
16
16
  游戏短通知字段:
@@ -61,11 +61,10 @@ ccl events --type speech
61
61
 
62
62
  ## 中途退出
63
63
 
64
- | 命令 | 适用时机 | 流行为 |
64
+ | 命令 | 适用时机 | 做什么 |
65
65
  |------|---------|--------|
66
- | `ccl game leave` | 匹配阶段,用户在匹配成功前退出 | 离开队列;`game start` owner 会退出。 |
67
- | `ccl game stop` | 用户只想停止本地监控/自动策略 | owner 收到 stop 指令后退出;短命令自己的 JSON 返回不变。 |
68
- | `ccl game quit` | 用户想离开当前对局并停止本地运行时 | owner 收到 quit 指令后退出;短命令自己的 JSON 返回后端离局结果。 |
66
+ | `ccl game leave` | 匹配/排队阶段,用户不想继续等 | 离开队列并结束等待。 |
67
+ | `ccl game quit` | 已进入对局,用户要退出本局;或本地 `game start` 卡住需要兜底清理 | 离开当前对局并停止本地运行时。 |
69
68
 
70
69
  如果宿主的 `TaskStop` 没能正确停掉 `ccl game start`,例如再次启动返回 `already_running` 或 `game-start.json` 仍在心跳,执行 `ccl game quit` 做兜底清理。
71
70
 
@@ -1,30 +1,30 @@
1
- import { Command } from 'commander';
2
- import { getWorkspaceDir } from '../lib/init-command.js';
3
- import { AuthStore } from '../lib/auth.js';
4
-
5
- export function createWorkspaceSubcommand(): Command {
6
- return new Command('workspace')
7
- .description('Print the workspace directory path')
8
- .action(() => {
9
- console.log(getWorkspaceDir());
10
- });
11
- }
12
-
13
- export function createApikeySubcommand(): Command {
14
- return new Command('apikey')
15
- .description('Print the active account API key')
16
- .action(() => {
17
- const store = new AuthStore();
18
- const profile = store.getActive();
19
- if (!profile) throw new Error('Not logged in. Run: clawclaw-cli account register');
20
- console.log(profile.apiKey);
21
- });
22
- }
23
-
24
- export function createConfigCommand(): Command {
25
- const config = new Command('config');
26
- config.description('Query ClawClaw CLI configuration.');
27
- config.addCommand(createWorkspaceSubcommand());
28
- config.addCommand(createApikeySubcommand());
29
- return config;
30
- }
1
+ import { Command } from 'commander';
2
+ import { getWorkspaceDir } from '../lib/init-command.js';
3
+ import { AuthStore } from '../lib/auth.js';
4
+
5
+ export function createWorkspaceSubcommand(): Command {
6
+ return new Command('workspace')
7
+ .description('Print the workspace directory path')
8
+ .action(() => {
9
+ console.log(getWorkspaceDir());
10
+ });
11
+ }
12
+
13
+ export function createApikeySubcommand(): Command {
14
+ return new Command('apikey')
15
+ .description('Print the active account API key')
16
+ .action(() => {
17
+ const store = new AuthStore();
18
+ const profile = store.getActive();
19
+ if (!profile) throw new Error('Not logged in. Run: clawclaw-cli account register');
20
+ console.log(profile.apiKey);
21
+ });
22
+ }
23
+
24
+ export function createConfigCommand(): Command {
25
+ const config = new Command('config');
26
+ config.description('Query ClawClaw CLI configuration.');
27
+ config.addCommand(createWorkspaceSubcommand());
28
+ config.addCommand(createApikeySubcommand());
29
+ return config;
30
+ }
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * `ccl setup codex` — 使用 Codex 原生 `codex mcp add` 安装 ClawClaw MCP server
3
3
  *
4
- * 优先: codex mcp add clawclaw -- <启动命令>
4
+ * 优先: codex mcp add clawclaw --env CLAWCLAW_WS_URL=... -- <启动命令>
5
5
  * 回退: 直接写 ~/.codex/config.toml(codex 不可用时)
6
6
  *
7
+ * 自动设置 CLAWCLAW_WS_URL,实现 clawclaw_game_start 零参数调用。
8
+ *
7
9
  * 用法:
8
- * ccl setup codex # dry-run 预览
9
- * ccl setup codex -y # 应用
10
- * ccl setup codex --print # 仅输出等价命令行
10
+ * ccl setup codex # dry-run 预览
11
+ * ccl setup codex -y # 应用(默认 ws://127.0.0.1:19997)
12
+ * ccl setup codex -y --ws-port 19998 # 指定端口
13
+ * ccl setup codex --print # 仅输出等价命令行
11
14
  */
12
15
 
13
16
  import { Command } from 'commander';
@@ -150,19 +153,22 @@ function codexMcpAlreadyRegistered(): boolean {
150
153
 
151
154
  export function createSetupCodexSubcommand(): Command {
152
155
  return new Command('codex')
153
- .description('使用 codex mcp add 安装 ClawClaw MCP server(codex 不可用时回退到写 config.toml)。')
156
+ .description('使用 codex mcp add 安装 ClawClaw MCP server(自动配置 CLAWCLAW_WS_URL,实现零参数启动游戏)。')
154
157
  .option('-y, --yes', '应用更改(默认 dry-run)')
155
158
  .option('--print', '仅输出推荐命令')
156
- .action((opts: { yes?: boolean; print?: boolean }) => {
159
+ .option('--ws-port <port>', 'app-server WebSocket 端口(默认 19997)', '19997')
160
+ .action((opts: { yes?: boolean; print?: boolean; wsPort?: string }) => {
161
+ const wsPort = opts.wsPort || '19997';
162
+ const wsUrl = `ws://127.0.0.1:${wsPort}`;
157
163
  const entry = resolveCodexClawclawEntry();
158
164
  if (!entry) {
159
165
  console.log('未找到 codex-clawclaw。');
160
- console.log('发布后: npm install -g codex-clawclaw');
166
+ console.log('安装: npm install -g @myclaw163/codex-clawclaw --registry https://registry.npmmirror.com/');
161
167
  console.log('本地开发: 在 codex-clawclaw 目录下 npm link');
162
168
  process.exit(1);
163
169
  }
164
170
 
165
- const addArgs = ['mcp', 'add', 'clawclaw', '--', entry.command, ...entry.args];
171
+ const addArgs = ['mcp', 'add', 'clawclaw', '--env', `CLAWCLAW_WS_URL=${wsUrl}`, '--', entry.command, ...entry.args];
166
172
 
167
173
  if (opts.print) {
168
174
  console.log('# 推荐执行:');
@@ -172,12 +178,14 @@ export function createSetupCodexSubcommand(): Command {
172
178
  console.log('[mcp_servers.clawclaw]');
173
179
  console.log(`command = '${entry.command.replace(/\\/g, '\\\\')}'`);
174
180
  console.log(`args = ${JSON.stringify(entry.args)}`);
181
+ console.log('[mcp_servers.clawclaw.env]');
182
+ console.log(`CLAWCLAW_WS_URL = '${wsUrl}'`);
175
183
  console.log('startup_timeout_sec = 30');
176
184
  return;
177
185
  }
178
186
 
179
187
  if (codexMcpAvailable() && codexMcpAlreadyRegistered()) {
180
- console.log('ClawClaw MCP server 已注册。');
188
+ console.log(`ClawClaw MCP server 已注册(CLWCLAW_WS_URL=${wsUrl})。`);
181
189
  process.exit(0);
182
190
  }
183
191
 
@@ -189,6 +197,7 @@ export function createSetupCodexSubcommand(): Command {
189
197
  console.log('(codex mcp 不可用,将回退到直接写 config.toml)');
190
198
  }
191
199
  console.log('');
200
+ console.log(`默认端口 ${wsPort},可用 --ws-port 指定。`);
192
201
  console.log('Dry-run 模式。加 -y 以应用更改。');
193
202
  process.exit(2);
194
203
  }
@@ -203,7 +212,12 @@ export function createSetupCodexSubcommand(): Command {
203
212
  console.error('codex mcp add 失败:', r.stderr?.trim() || r.stdout?.trim() || `exit ${r.status}`);
204
213
  process.exit(1);
205
214
  }
206
- console.log('已注册 ClawClaw MCP server(无需重启 Codex)。');
215
+ console.log(`已注册 ClawClaw MCP server(CLAWCLAW_WS_URL=${wsUrl})。`);
216
+ console.log('');
217
+ console.log('下一步:');
218
+ console.log(` 1. codex app-server --listen ${wsUrl}`);
219
+ console.log(` 2. codex --remote ${wsUrl}`);
220
+ console.log(' 3. 在会话中说 开始一局龙虾杀(无需传参数)');
207
221
  process.exit(0);
208
222
  }
209
223
 
@@ -236,10 +250,14 @@ export function createSetupCodexSubcommand(): Command {
236
250
  `args = ${JSON.stringify(entry.args)}`,
237
251
  'startup_timeout_sec = 30',
238
252
  '',
253
+ '[mcp_servers.clawclaw.env]',
254
+ `CLAWCLAW_WS_URL = '${wsUrl}'`,
255
+ '',
239
256
  ].join('\n');
240
257
  appendFileSync(configPath, snippet, 'utf-8');
241
258
 
242
259
  console.log(`已更新 ${configPath}(备份: config.toml.bak.${ts})`);
260
+ console.log(`CLAWCLAW_WS_URL = ${wsUrl}`);
243
261
  console.log('');
244
262
  console.log('重启 Codex 后生效。可用工具:');
245
263
  console.log(' clawclaw_game_start / clawclaw_game_stop / clawclaw_game_status');
@@ -1,96 +1,96 @@
1
- import { describe, expect, it, vi, beforeEach } from 'vitest';
2
- import { runHermesSetup } from './hermes.js';
3
- import { spawnSync } from 'child_process';
4
-
5
- vi.mock('child_process', () => ({
6
- spawnSync: vi.fn(),
7
- }));
8
-
9
- const mockedSpawnSync = vi.mocked(spawnSync);
10
-
11
- function mockSpawn(overrides: Array<{ status: number; stdout?: string; stderr?: string }>) {
12
- mockedSpawnSync.mockImplementation((_cmd: any, _args?: any, _opts?: any): any => {
13
- const next = overrides.shift();
14
- if (!next) throw new Error('Unexpected spawnSync call');
15
- return {
16
- status: next.status,
17
- stdout: next.stdout ?? '',
18
- stderr: next.stderr ?? '',
19
- };
20
- });
21
- }
22
-
23
- beforeEach(() => {
24
- vi.clearAllMocks();
25
- });
26
-
27
- describe('runHermesSetup', () => {
28
- it('returns error when hermes CLI is not found', () => {
29
- mockSpawn([{ status: 1, stderr: 'command not found' }]);
30
- const r = runHermesSetup({});
31
- expect(r.exitCode).toBe(1);
32
- expect(r.output.some((l) => l.includes('not found'))).toBe(true);
33
- });
34
-
35
- it('reports already-enabled when clawclaw is in plugins list', () => {
36
- mockSpawn([
37
- { status: 0 }, // --version
38
- { status: 0, stdout: 'clawclaw\nother-plugin' }, // plugins list
39
- ]);
40
- const r = runHermesSetup({});
41
- expect(r.exitCode).toBe(0);
42
- expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
43
- });
44
-
45
- it('dry-run shows pending message when not enabled', () => {
46
- mockSpawn([
47
- { status: 0 }, // --version
48
- { status: 0, stdout: 'other-plugin' }, // plugins list
49
- ]);
50
- const r = runHermesSetup({});
51
- expect(r.exitCode).toBe(2);
52
- expect(r.output.some((l) => l.includes('Dry-run'))).toBe(true);
53
- });
54
-
55
- it('--print shows the command that would run', () => {
56
- mockSpawn([
57
- { status: 0 }, // --version
58
- { status: 0, stdout: 'other-plugin' }, // plugins list
59
- ]);
60
- const r = runHermesSetup({ print: true });
61
- expect(r.exitCode).toBe(0);
62
- expect(r.output.some((l) => l.includes('hermes plugins enable clawclaw'))).toBe(true);
63
- });
64
-
65
- it('-y runs hermes plugins enable and succeeds', () => {
66
- mockSpawn([
67
- { status: 0 }, // --version
68
- { status: 0, stdout: 'other-plugin' }, // plugins list
69
- { status: 0, stdout: 'plugin enabled' }, // plugins enable
70
- ]);
71
- const r = runHermesSetup({ yes: true });
72
- expect(r.exitCode).toBe(0);
73
- expect(r.output.some((l) => l.includes('enabled'))).toBe(true);
74
- });
75
-
76
- it('-y handles hermes plugins enable failure', () => {
77
- mockSpawn([
78
- { status: 0 }, // --version
79
- { status: 0, stdout: 'other-plugin' }, // plugins list
80
- { status: 1, stderr: 'permission denied' }, // plugins enable
81
- ]);
82
- const r = runHermesSetup({ yes: true });
83
- expect(r.exitCode).toBe(1);
84
- expect(r.output.some((l) => l.includes('Failed'))).toBe(true);
85
- });
86
-
87
- it('already-enabled short-circuits --print', () => {
88
- mockSpawn([
89
- { status: 0 }, // --version
90
- { status: 0, stdout: 'clawclaw' }, // plugins list
91
- ]);
92
- const r = runHermesSetup({ print: true });
93
- expect(r.exitCode).toBe(0);
94
- expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
95
- });
96
- });
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { runHermesSetup } from './hermes.js';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ vi.mock('child_process', () => ({
6
+ spawnSync: vi.fn(),
7
+ }));
8
+
9
+ const mockedSpawnSync = vi.mocked(spawnSync);
10
+
11
+ function mockSpawn(overrides: Array<{ status: number; stdout?: string; stderr?: string }>) {
12
+ mockedSpawnSync.mockImplementation((_cmd: any, _args?: any, _opts?: any): any => {
13
+ const next = overrides.shift();
14
+ if (!next) throw new Error('Unexpected spawnSync call');
15
+ return {
16
+ status: next.status,
17
+ stdout: next.stdout ?? '',
18
+ stderr: next.stderr ?? '',
19
+ };
20
+ });
21
+ }
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe('runHermesSetup', () => {
28
+ it('returns error when hermes CLI is not found', () => {
29
+ mockSpawn([{ status: 1, stderr: 'command not found' }]);
30
+ const r = runHermesSetup({});
31
+ expect(r.exitCode).toBe(1);
32
+ expect(r.output.some((l) => l.includes('not found'))).toBe(true);
33
+ });
34
+
35
+ it('reports already-enabled when clawclaw is in plugins list', () => {
36
+ mockSpawn([
37
+ { status: 0 }, // --version
38
+ { status: 0, stdout: 'clawclaw\nother-plugin' }, // plugins list
39
+ ]);
40
+ const r = runHermesSetup({});
41
+ expect(r.exitCode).toBe(0);
42
+ expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
43
+ });
44
+
45
+ it('dry-run shows pending message when not enabled', () => {
46
+ mockSpawn([
47
+ { status: 0 }, // --version
48
+ { status: 0, stdout: 'other-plugin' }, // plugins list
49
+ ]);
50
+ const r = runHermesSetup({});
51
+ expect(r.exitCode).toBe(2);
52
+ expect(r.output.some((l) => l.includes('Dry-run'))).toBe(true);
53
+ });
54
+
55
+ it('--print shows the command that would run', () => {
56
+ mockSpawn([
57
+ { status: 0 }, // --version
58
+ { status: 0, stdout: 'other-plugin' }, // plugins list
59
+ ]);
60
+ const r = runHermesSetup({ print: true });
61
+ expect(r.exitCode).toBe(0);
62
+ expect(r.output.some((l) => l.includes('hermes plugins enable clawclaw'))).toBe(true);
63
+ });
64
+
65
+ it('-y runs hermes plugins enable and succeeds', () => {
66
+ mockSpawn([
67
+ { status: 0 }, // --version
68
+ { status: 0, stdout: 'other-plugin' }, // plugins list
69
+ { status: 0, stdout: 'plugin enabled' }, // plugins enable
70
+ ]);
71
+ const r = runHermesSetup({ yes: true });
72
+ expect(r.exitCode).toBe(0);
73
+ expect(r.output.some((l) => l.includes('enabled'))).toBe(true);
74
+ });
75
+
76
+ it('-y handles hermes plugins enable failure', () => {
77
+ mockSpawn([
78
+ { status: 0 }, // --version
79
+ { status: 0, stdout: 'other-plugin' }, // plugins list
80
+ { status: 1, stderr: 'permission denied' }, // plugins enable
81
+ ]);
82
+ const r = runHermesSetup({ yes: true });
83
+ expect(r.exitCode).toBe(1);
84
+ expect(r.output.some((l) => l.includes('Failed'))).toBe(true);
85
+ });
86
+
87
+ it('already-enabled short-circuits --print', () => {
88
+ mockSpawn([
89
+ { status: 0 }, // --version
90
+ { status: 0, stdout: 'clawclaw' }, // plugins list
91
+ ]);
92
+ const r = runHermesSetup({ print: true });
93
+ expect(r.exitCode).toBe(0);
94
+ expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
95
+ });
96
+ });
@@ -1,76 +1,76 @@
1
- /**
2
- * `ccl setup hermes` — enable the clawclaw plugin in Hermes via `hermes plugins enable`.
3
- */
4
-
5
- import { Command } from 'commander';
6
- import { spawnSync } from 'child_process';
7
-
8
- export const PLUGIN_ID = 'clawclaw';
9
-
10
- export interface SetupHermesResult {
11
- exitCode: number;
12
- output: string[];
13
- }
14
-
15
- export function runHermesSetup(flags: { yes?: boolean; print?: boolean }): SetupHermesResult {
16
- const out: string[] = [];
17
-
18
- // Check hermes is available
19
- const which = spawnSync('hermes', ['--version'], { stdio: 'pipe', timeout: 5000 });
20
- if (which.status !== 0) {
21
- out.push('hermes CLI not found. Install it first, then re-run.');
22
- return { exitCode: 1, output: out };
23
- }
24
-
25
- // Check current plugin status
26
- const list = spawnSync('hermes', ['plugins', 'list'], {
27
- stdio: 'pipe',
28
- timeout: 5000,
29
- env: { ...process.env, HERMES_PLUGINS_DEBUG: '1' },
30
- });
31
- const listOutput = list.stdout?.toString() ?? '';
32
- const alreadyEnabled = listOutput.includes(PLUGIN_ID) && !listOutput.includes(`${PLUGIN_ID} (disabled)`);
33
-
34
- if (alreadyEnabled) {
35
- out.push(`Plugin "${PLUGIN_ID}" is already enabled in Hermes.`);
36
- return { exitCode: 0, output: out };
37
- }
38
-
39
- if (flags.print) {
40
- out.push(`# Will run: hermes plugins enable ${PLUGIN_ID}`);
41
- return { exitCode: 0, output: out };
42
- }
43
-
44
- if (!flags.yes) {
45
- out.push(`Pending: hermes plugins enable ${PLUGIN_ID}`);
46
- out.push(``);
47
- out.push(`Dry-run only. Re-run with -y to apply.`);
48
- return { exitCode: 2, output: out };
49
- }
50
-
51
- const result = spawnSync('hermes', ['plugins', 'enable', PLUGIN_ID], {
52
- stdio: 'pipe',
53
- timeout: 10000,
54
- });
55
-
56
- if (result.status !== 0) {
57
- out.push(`Failed to enable plugin: ${result.stderr?.toString() ?? result.stdout?.toString() ?? 'unknown error'}`);
58
- return { exitCode: 1, output: out };
59
- }
60
-
61
- out.push(`Plugin "${PLUGIN_ID}" enabled in Hermes.`);
62
- out.push(`Verify: HERMES_PLUGINS_DEBUG=1 hermes plugins list`);
63
- return { exitCode: 0, output: out };
64
- }
65
-
66
- export function createSetupHermesSubcommand(): Command {
67
- return new Command('hermes')
68
- .description('Enable the clawclaw plugin in Hermes via "hermes plugins enable".')
69
- .option('-y, --yes', 'Apply changes (default is dry-run)')
70
- .option('--print', 'Only print the command that would be run; do not execute')
71
- .action((opts: { yes?: boolean; print?: boolean }) => {
72
- const result = runHermesSetup({ yes: opts.yes, print: opts.print });
73
- for (const line of result.output) console.log(line);
74
- if (result.exitCode !== 0) process.exit(result.exitCode);
75
- });
76
- }
1
+ /**
2
+ * `ccl setup hermes` — enable the clawclaw plugin in Hermes via `hermes plugins enable`.
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { spawnSync } from 'child_process';
7
+
8
+ export const PLUGIN_ID = 'clawclaw';
9
+
10
+ export interface SetupHermesResult {
11
+ exitCode: number;
12
+ output: string[];
13
+ }
14
+
15
+ export function runHermesSetup(flags: { yes?: boolean; print?: boolean }): SetupHermesResult {
16
+ const out: string[] = [];
17
+
18
+ // Check hermes is available
19
+ const which = spawnSync('hermes', ['--version'], { stdio: 'pipe', timeout: 5000 });
20
+ if (which.status !== 0) {
21
+ out.push('hermes CLI not found. Install it first, then re-run.');
22
+ return { exitCode: 1, output: out };
23
+ }
24
+
25
+ // Check current plugin status
26
+ const list = spawnSync('hermes', ['plugins', 'list'], {
27
+ stdio: 'pipe',
28
+ timeout: 5000,
29
+ env: { ...process.env, HERMES_PLUGINS_DEBUG: '1' },
30
+ });
31
+ const listOutput = list.stdout?.toString() ?? '';
32
+ const alreadyEnabled = listOutput.includes(PLUGIN_ID) && !listOutput.includes(`${PLUGIN_ID} (disabled)`);
33
+
34
+ if (alreadyEnabled) {
35
+ out.push(`Plugin "${PLUGIN_ID}" is already enabled in Hermes.`);
36
+ return { exitCode: 0, output: out };
37
+ }
38
+
39
+ if (flags.print) {
40
+ out.push(`# Will run: hermes plugins enable ${PLUGIN_ID}`);
41
+ return { exitCode: 0, output: out };
42
+ }
43
+
44
+ if (!flags.yes) {
45
+ out.push(`Pending: hermes plugins enable ${PLUGIN_ID}`);
46
+ out.push(``);
47
+ out.push(`Dry-run only. Re-run with -y to apply.`);
48
+ return { exitCode: 2, output: out };
49
+ }
50
+
51
+ const result = spawnSync('hermes', ['plugins', 'enable', PLUGIN_ID], {
52
+ stdio: 'pipe',
53
+ timeout: 10000,
54
+ });
55
+
56
+ if (result.status !== 0) {
57
+ out.push(`Failed to enable plugin: ${result.stderr?.toString() ?? result.stdout?.toString() ?? 'unknown error'}`);
58
+ return { exitCode: 1, output: out };
59
+ }
60
+
61
+ out.push(`Plugin "${PLUGIN_ID}" enabled in Hermes.`);
62
+ out.push(`Verify: HERMES_PLUGINS_DEBUG=1 hermes plugins list`);
63
+ return { exitCode: 0, output: out };
64
+ }
65
+
66
+ export function createSetupHermesSubcommand(): Command {
67
+ return new Command('hermes')
68
+ .description('Enable the clawclaw plugin in Hermes via "hermes plugins enable".')
69
+ .option('-y, --yes', 'Apply changes (default is dry-run)')
70
+ .option('--print', 'Only print the command that would be run; do not execute')
71
+ .action((opts: { yes?: boolean; print?: boolean }) => {
72
+ const result = runHermesSetup({ yes: opts.yes, print: opts.print });
73
+ for (const line of result.output) console.log(line);
74
+ if (result.exitCode !== 0) process.exit(result.exitCode);
75
+ });
76
+ }