@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.
- package/bin/clawclaw-cli.mjs +3 -3
- package/package.json +1 -1
- package/scripts/sync-bundled-skill.mjs +1 -1
- package/skills/clawclaw/SKILL.md +11 -11
- package/skills/clawclaw/references/GAME-MECHANICS.md +2 -0
- package/skills/clawclaw/references/STREAM.md +4 -5
- package/src/commands/config.ts +30 -30
- package/src/commands/setup/codex.ts +28 -10
- package/src/commands/setup/hermes.test.ts +96 -96
- package/src/commands/setup/hermes.ts +76 -76
- package/src/commands/setup/index.ts +13 -13
- package/src/commands/setup/openclaw.test.ts +114 -114
- package/src/commands/setup/openclaw.ts +147 -147
- package/src/lib/host-config-patcher.test.ts +130 -130
- package/src/lib/host-config-patcher.ts +151 -151
- package/src/lib/hub-reminder.ts +19 -19
package/bin/clawclaw-cli.mjs
CHANGED
|
@@ -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
package/skills/clawclaw/SKILL.md
CHANGED
|
@@ -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.
|
|
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: '
|
|
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` | 会议结束 |
|
|
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`
|
|
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` |
|
|
222
|
-
| `ccl game
|
|
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 在排队、分配、超时、手动
|
|
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` |
|
|
67
|
-
| `ccl game
|
|
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
|
|
package/src/commands/config.ts
CHANGED
|
@@ -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
|
|
9
|
-
* ccl setup codex -y
|
|
10
|
-
* ccl setup codex --
|
|
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(
|
|
156
|
+
.description('使用 codex mcp add 安装 ClawClaw MCP server(自动配置 CLAWCLAW_WS_URL,实现零参数启动游戏)。')
|
|
154
157
|
.option('-y, --yes', '应用更改(默认 dry-run)')
|
|
155
158
|
.option('--print', '仅输出推荐命令')
|
|
156
|
-
.
|
|
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('
|
|
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(
|
|
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(
|
|
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
|
+
}
|