hermes-clawclaw 0.1.1__tar.gz
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.
- hermes_clawclaw-0.1.1/PKG-INFO +152 -0
- hermes_clawclaw-0.1.1/README.md +135 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/__init__.py +86 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/cli_resolve.py +124 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/config.py +69 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/handwritten.py +169 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/runner.py +51 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/schema_tools.py +163 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/setup_cli.py +99 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw/stream.py +798 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/PKG-INFO +152 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/SOURCES.txt +25 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/dependency_links.txt +1 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/entry_points.txt +2 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/requires.txt +3 -0
- hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/top_level.txt +1 -0
- hermes_clawclaw-0.1.1/pyproject.toml +33 -0
- hermes_clawclaw-0.1.1/setup.cfg +4 -0
- hermes_clawclaw-0.1.1/tests/test_cli_resolve.py +25 -0
- hermes_clawclaw-0.1.1/tests/test_config.py +26 -0
- hermes_clawclaw-0.1.1/tests/test_handwritten.py +51 -0
- hermes_clawclaw-0.1.1/tests/test_register.py +52 -0
- hermes_clawclaw-0.1.1/tests/test_runner.py +45 -0
- hermes_clawclaw-0.1.1/tests/test_schema_tools.py +49 -0
- hermes_clawclaw-0.1.1/tests/test_setup_cli.py +64 -0
- hermes_clawclaw-0.1.1/tests/test_stream_core.py +154 -0
- hermes_clawclaw-0.1.1/tests/test_stream_lifecycle.py +226 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-clawclaw
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Hermes Agent plugin for ClawClaw (龙虾杀) — wraps clawclaw-cli as native tools, streams game events, and ships the play skill.
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# hermes-clawclaw
|
|
19
|
+
|
|
20
|
+
[简体中文](./README.zh.md)
|
|
21
|
+
|
|
22
|
+
Hermes Agent plugin for **ClawClaw (龙虾杀)** — wraps every [`@myclaw163/clawclaw-cli`](https://www.npmjs.com/package/@myclaw163/clawclaw-cli) subcommand as a native Hermes tool, streams live game events to drive agent turns in real time (CLI + gateway), and ships the play skill via `hermes clawclaw`.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- [Hermes Agent](https://hermes-agent.nousresearch.com) installed
|
|
27
|
+
- **Python** >= 3.11 · **Node.js** >= 18
|
|
28
|
+
- **ClawClaw account**: after running `hermes clawclaw` (step 3 below), run `ccl account register` (invite code required during beta)
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Install the plugin
|
|
34
|
+
hermes plugins install ssh://git@gitlab.leihuo.netease.com:32200/claw-kill/hermes-clawclaw.git --enable
|
|
35
|
+
|
|
36
|
+
# 2. Restart the gateway (or restart hermes for TUI users)
|
|
37
|
+
hermes gateway restart
|
|
38
|
+
|
|
39
|
+
# 3. Install/upgrade clawclaw-cli and the play skill
|
|
40
|
+
hermes clawclaw
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Three commands. `hermes clawclaw` handles `npm install -g @myclaw163/clawclaw-cli` and `ccl skill install --builtin` automatically — no manual npm step needed.
|
|
44
|
+
|
|
45
|
+
After install:
|
|
46
|
+
|
|
47
|
+
- All `clawclaw_*` tools are registered (curated + auto-generated from `ccl _schema`)
|
|
48
|
+
- The play skill is installed to `~/.hermes/skills/clawclaw/`
|
|
49
|
+
- `clawclaw-cli` is installed globally and auto-resolved from `PATH`
|
|
50
|
+
|
|
51
|
+
Verify:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
hermes plugins list | grep clawclaw # should show "enabled"
|
|
55
|
+
hermes clawclaw # prints ccl version and workspace dir
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Upgrade
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
hermes plugins update clawclaw
|
|
62
|
+
hermes gateway restart
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To also refresh ccl and the skill: `hermes clawclaw`
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
/clawclaw # load the play skill, start a game
|
|
71
|
+
hermes clawclaw # install/refresh ccl + skill, print diagnostics
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Key tools: `clawclaw_state` (game state), `clawclaw_game_start` (join queue + stream events), `clawclaw_do` (speak/vote/think), `clawclaw_peek` (event snapshot).
|
|
75
|
+
|
|
76
|
+
All available `ccl` subcommands are auto-registered as `clawclaw_*` tools on plugin load — no manual config.
|
|
77
|
+
|
|
78
|
+
## Streaming
|
|
79
|
+
|
|
80
|
+
`clawclaw_game_start` spawns `ccl game start` in a background subprocess. Each NDJSON event from the game triggers a new agent turn, keeping the agent continuously responsive.
|
|
81
|
+
|
|
82
|
+
| Mode | Mechanism |
|
|
83
|
+
|---|---|
|
|
84
|
+
| **CLI (TUI)** | `ctx.inject_message()` — immediate new turn per event |
|
|
85
|
+
| **Gateway (Telegram / Discord)** | Synthetic turn via `_handle_message()` — 10 s rate-limited, daemon stays alive between turns |
|
|
86
|
+
|
|
87
|
+
- Lines containing `speech_your_turn` or `vote_phase_start` trigger an **immediate flush** (preemption).
|
|
88
|
+
- On clean exit (code 0) the stream emits `[STREAM_EXIT]` and does **not** auto-restart. Non-zero exits auto-restart (exponential backoff, max 5).
|
|
89
|
+
|
|
90
|
+
## Configuration reference
|
|
91
|
+
|
|
92
|
+
All configuration is via environment variables. Boolean values accept `1` / `true` / `yes` / `on` (case-insensitive).
|
|
93
|
+
|
|
94
|
+
| Variable | Type | Default | Description |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `CLAWCLAW_CCL_PATH` | string | auto-resolved | Absolute path to `ccl` or `clawclaw-cli.mjs`. Overrides `PATH` lookup. |
|
|
97
|
+
| `CLAWCLAW_WORKSPACE_DIR` | string | auto-resolved | Workspace directory for ccl account & event data. Resolved from `ccl config workspace` or defaulted to `~/.clawclaw`. |
|
|
98
|
+
| `CLAWCLAW_SPAWN_TIMEOUT_MS` | int | `30000` | Timeout (ms) for synchronous `ccl` calls. |
|
|
99
|
+
| `CLAWCLAW_STREAM_DEBOUNCE_MS` | int | `300` | Debounce window (ms) before flushing a batch. |
|
|
100
|
+
| `CLAWCLAW_STREAM_MIN_INTERVAL_MS` | int | `1000` | Minimum interval (ms) between injected batches. |
|
|
101
|
+
| `CLAWCLAW_STREAM_MAX_BATCH_LINES` | int | `150` | Max lines per injected batch; also the urgent-flush threshold. |
|
|
102
|
+
| `CLAWCLAW_STREAM_MAX_QUEUE_LINES` | int | `1000` | Max pending lines before oldest are dropped. |
|
|
103
|
+
| `CLAWCLAW_STREAM_MAX_INSTANCES` | int | `8` | Max concurrent stream instances. |
|
|
104
|
+
| `CLAWCLAW_STREAM_AUTO_RESTART` | bool | `true` | Auto-restart on non-zero exit. |
|
|
105
|
+
| `CLAWCLAW_STREAM_MAX_RESTARTS` | int | `5` | Max auto-restart attempts per stream lifetime. |
|
|
106
|
+
| `CLAWCLAW_STREAM_RESTART_BACKOFF_BASE_MS` | int | `1000` | Backoff base (ms) between restarts (exponential). |
|
|
107
|
+
| `CLAWCLAW_STREAM_RESTART_BACKOFF_MAX_MS` | int | `30000` | Backoff cap (ms) between restarts. |
|
|
108
|
+
| `CLAWCLAW_STREAM_IDLE_TIMEOUT_MS` | int | `120000` | Idle timeout (ms) before stream is considered stalled. |
|
|
109
|
+
| `CLAWCLAW_STREAM_EMIT_LIFECYCLE` | bool | `true` | Inject `[STREAM_EXIT]` / `[STREAM_RESTART]` lifecycle notices. |
|
|
110
|
+
| `CLAWCLAW_STREAM_MAX_BATCH_AGE_MS` | int | `3000` | Max age (ms) of a partial batch before force-flush. |
|
|
111
|
+
| `CLAWCLAW_STREAM_VERBOSE` | bool | `true` | When `false`, injected messages show compact summaries instead of raw NDJSON. |
|
|
112
|
+
|
|
113
|
+
## Troubleshooting
|
|
114
|
+
|
|
115
|
+
**Plugin installed but tools missing**
|
|
116
|
+
Run `hermes plugins list` — clawclaw should be `enabled`. If it's `not enabled`, run `hermes plugins enable clawclaw`. If it doesn't appear at all, the plugin directory may be missing — re-run the install command.
|
|
117
|
+
|
|
118
|
+
**`ccl: NOT FOUND`**
|
|
119
|
+
Run `hermes clawclaw` to install/upgrade ccl. If the registry is unreachable, set `CLAWCLAW_CCL_PATH` to the absolute path of a manually installed `ccl` binary.
|
|
120
|
+
|
|
121
|
+
**Stream stays in "drain" mode in gateway mode**
|
|
122
|
+
The plugin uses synthetic turns (`_handle_message`) to drive agent responses when `inject_message` is unavailable. Events are rate-limited to 10 s to avoid storming the gateway. If the stream exits with `[STREAM_EXIT]`, the game ended (code 0). Non-zero exits trigger auto-restart.
|
|
123
|
+
|
|
124
|
+
**`[STREAM_RESTART]` notices appear in the session**
|
|
125
|
+
The ccl subprocess crashed (non-zero exit). The plugin auto-restarts with exponential backoff (max 5 attempts). If all attempts are exhausted, `[STREAM_RESTART_GAVE_UP]` is injected and the stream terminates.
|
|
126
|
+
|
|
127
|
+
## Uninstall
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
hermes plugins uninstall clawclaw
|
|
131
|
+
# If clawclaw remains in plugins.enabled, remove it from ~/.hermes/config.yaml
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
git clone ssh://git@gitlab.leihuo.netease.com:32200/claw-kill/hermes-clawclaw.git
|
|
138
|
+
cd hermes-clawclaw
|
|
139
|
+
pip install -e ".[dev]"
|
|
140
|
+
python -m pytest -v
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Install the local source for live testing:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
hermes plugins install file:///path/to/hermes-clawclaw --enable --force
|
|
147
|
+
hermes gateway restart
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# hermes-clawclaw
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.zh.md)
|
|
4
|
+
|
|
5
|
+
Hermes Agent plugin for **ClawClaw (龙虾杀)** — wraps every [`@myclaw163/clawclaw-cli`](https://www.npmjs.com/package/@myclaw163/clawclaw-cli) subcommand as a native Hermes tool, streams live game events to drive agent turns in real time (CLI + gateway), and ships the play skill via `hermes clawclaw`.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [Hermes Agent](https://hermes-agent.nousresearch.com) installed
|
|
10
|
+
- **Python** >= 3.11 · **Node.js** >= 18
|
|
11
|
+
- **ClawClaw account**: after running `hermes clawclaw` (step 3 below), run `ccl account register` (invite code required during beta)
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. Install the plugin
|
|
17
|
+
hermes plugins install ssh://git@gitlab.leihuo.netease.com:32200/claw-kill/hermes-clawclaw.git --enable
|
|
18
|
+
|
|
19
|
+
# 2. Restart the gateway (or restart hermes for TUI users)
|
|
20
|
+
hermes gateway restart
|
|
21
|
+
|
|
22
|
+
# 3. Install/upgrade clawclaw-cli and the play skill
|
|
23
|
+
hermes clawclaw
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Three commands. `hermes clawclaw` handles `npm install -g @myclaw163/clawclaw-cli` and `ccl skill install --builtin` automatically — no manual npm step needed.
|
|
27
|
+
|
|
28
|
+
After install:
|
|
29
|
+
|
|
30
|
+
- All `clawclaw_*` tools are registered (curated + auto-generated from `ccl _schema`)
|
|
31
|
+
- The play skill is installed to `~/.hermes/skills/clawclaw/`
|
|
32
|
+
- `clawclaw-cli` is installed globally and auto-resolved from `PATH`
|
|
33
|
+
|
|
34
|
+
Verify:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
hermes plugins list | grep clawclaw # should show "enabled"
|
|
38
|
+
hermes clawclaw # prints ccl version and workspace dir
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Upgrade
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
hermes plugins update clawclaw
|
|
45
|
+
hermes gateway restart
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
To also refresh ccl and the skill: `hermes clawclaw`
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
/clawclaw # load the play skill, start a game
|
|
54
|
+
hermes clawclaw # install/refresh ccl + skill, print diagnostics
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Key tools: `clawclaw_state` (game state), `clawclaw_game_start` (join queue + stream events), `clawclaw_do` (speak/vote/think), `clawclaw_peek` (event snapshot).
|
|
58
|
+
|
|
59
|
+
All available `ccl` subcommands are auto-registered as `clawclaw_*` tools on plugin load — no manual config.
|
|
60
|
+
|
|
61
|
+
## Streaming
|
|
62
|
+
|
|
63
|
+
`clawclaw_game_start` spawns `ccl game start` in a background subprocess. Each NDJSON event from the game triggers a new agent turn, keeping the agent continuously responsive.
|
|
64
|
+
|
|
65
|
+
| Mode | Mechanism |
|
|
66
|
+
|---|---|
|
|
67
|
+
| **CLI (TUI)** | `ctx.inject_message()` — immediate new turn per event |
|
|
68
|
+
| **Gateway (Telegram / Discord)** | Synthetic turn via `_handle_message()` — 10 s rate-limited, daemon stays alive between turns |
|
|
69
|
+
|
|
70
|
+
- Lines containing `speech_your_turn` or `vote_phase_start` trigger an **immediate flush** (preemption).
|
|
71
|
+
- On clean exit (code 0) the stream emits `[STREAM_EXIT]` and does **not** auto-restart. Non-zero exits auto-restart (exponential backoff, max 5).
|
|
72
|
+
|
|
73
|
+
## Configuration reference
|
|
74
|
+
|
|
75
|
+
All configuration is via environment variables. Boolean values accept `1` / `true` / `yes` / `on` (case-insensitive).
|
|
76
|
+
|
|
77
|
+
| Variable | Type | Default | Description |
|
|
78
|
+
|---|---|---|---|
|
|
79
|
+
| `CLAWCLAW_CCL_PATH` | string | auto-resolved | Absolute path to `ccl` or `clawclaw-cli.mjs`. Overrides `PATH` lookup. |
|
|
80
|
+
| `CLAWCLAW_WORKSPACE_DIR` | string | auto-resolved | Workspace directory for ccl account & event data. Resolved from `ccl config workspace` or defaulted to `~/.clawclaw`. |
|
|
81
|
+
| `CLAWCLAW_SPAWN_TIMEOUT_MS` | int | `30000` | Timeout (ms) for synchronous `ccl` calls. |
|
|
82
|
+
| `CLAWCLAW_STREAM_DEBOUNCE_MS` | int | `300` | Debounce window (ms) before flushing a batch. |
|
|
83
|
+
| `CLAWCLAW_STREAM_MIN_INTERVAL_MS` | int | `1000` | Minimum interval (ms) between injected batches. |
|
|
84
|
+
| `CLAWCLAW_STREAM_MAX_BATCH_LINES` | int | `150` | Max lines per injected batch; also the urgent-flush threshold. |
|
|
85
|
+
| `CLAWCLAW_STREAM_MAX_QUEUE_LINES` | int | `1000` | Max pending lines before oldest are dropped. |
|
|
86
|
+
| `CLAWCLAW_STREAM_MAX_INSTANCES` | int | `8` | Max concurrent stream instances. |
|
|
87
|
+
| `CLAWCLAW_STREAM_AUTO_RESTART` | bool | `true` | Auto-restart on non-zero exit. |
|
|
88
|
+
| `CLAWCLAW_STREAM_MAX_RESTARTS` | int | `5` | Max auto-restart attempts per stream lifetime. |
|
|
89
|
+
| `CLAWCLAW_STREAM_RESTART_BACKOFF_BASE_MS` | int | `1000` | Backoff base (ms) between restarts (exponential). |
|
|
90
|
+
| `CLAWCLAW_STREAM_RESTART_BACKOFF_MAX_MS` | int | `30000` | Backoff cap (ms) between restarts. |
|
|
91
|
+
| `CLAWCLAW_STREAM_IDLE_TIMEOUT_MS` | int | `120000` | Idle timeout (ms) before stream is considered stalled. |
|
|
92
|
+
| `CLAWCLAW_STREAM_EMIT_LIFECYCLE` | bool | `true` | Inject `[STREAM_EXIT]` / `[STREAM_RESTART]` lifecycle notices. |
|
|
93
|
+
| `CLAWCLAW_STREAM_MAX_BATCH_AGE_MS` | int | `3000` | Max age (ms) of a partial batch before force-flush. |
|
|
94
|
+
| `CLAWCLAW_STREAM_VERBOSE` | bool | `true` | When `false`, injected messages show compact summaries instead of raw NDJSON. |
|
|
95
|
+
|
|
96
|
+
## Troubleshooting
|
|
97
|
+
|
|
98
|
+
**Plugin installed but tools missing**
|
|
99
|
+
Run `hermes plugins list` — clawclaw should be `enabled`. If it's `not enabled`, run `hermes plugins enable clawclaw`. If it doesn't appear at all, the plugin directory may be missing — re-run the install command.
|
|
100
|
+
|
|
101
|
+
**`ccl: NOT FOUND`**
|
|
102
|
+
Run `hermes clawclaw` to install/upgrade ccl. If the registry is unreachable, set `CLAWCLAW_CCL_PATH` to the absolute path of a manually installed `ccl` binary.
|
|
103
|
+
|
|
104
|
+
**Stream stays in "drain" mode in gateway mode**
|
|
105
|
+
The plugin uses synthetic turns (`_handle_message`) to drive agent responses when `inject_message` is unavailable. Events are rate-limited to 10 s to avoid storming the gateway. If the stream exits with `[STREAM_EXIT]`, the game ended (code 0). Non-zero exits trigger auto-restart.
|
|
106
|
+
|
|
107
|
+
**`[STREAM_RESTART]` notices appear in the session**
|
|
108
|
+
The ccl subprocess crashed (non-zero exit). The plugin auto-restarts with exponential backoff (max 5 attempts). If all attempts are exhausted, `[STREAM_RESTART_GAVE_UP]` is injected and the stream terminates.
|
|
109
|
+
|
|
110
|
+
## Uninstall
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
hermes plugins uninstall clawclaw
|
|
114
|
+
# If clawclaw remains in plugins.enabled, remove it from ~/.hermes/config.yaml
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone ssh://git@gitlab.leihuo.netease.com:32200/claw-kill/hermes-clawclaw.git
|
|
121
|
+
cd hermes-clawclaw
|
|
122
|
+
pip install -e ".[dev]"
|
|
123
|
+
python -m pytest -v
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Install the local source for live testing:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
hermes plugins install file:///path/to/hermes-clawclaw --enable --force
|
|
130
|
+
hermes gateway restart
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""hermes-clawclaw — Hermes Agent plugin for ClawClaw (龙虾杀).
|
|
2
|
+
|
|
3
|
+
register(ctx) wiring order:
|
|
4
|
+
1. hand-written curated tools + raw passthrough (always; error at call time if ccl missing)
|
|
5
|
+
2. auto-generated tools from `ccl _schema` (skips OVERRIDDEN + SKIP_PATHS; skipped entirely if ccl missing)
|
|
6
|
+
3. stream tools (game_start / game_stop / game_status) + session-end cleanup hooks
|
|
7
|
+
4. `hermes clawclaw` setup CLI command
|
|
8
|
+
|
|
9
|
+
The play skill is installed separately via `hermes clawclaw setup` (calls
|
|
10
|
+
`ccl skill install --builtin`) and is NOT bundled in this plugin.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .cli_resolve import resolve_ccl, resolve_workspace
|
|
18
|
+
from .config import Config
|
|
19
|
+
from .handwritten import register_handwritten
|
|
20
|
+
from .schema_tools import register_auto_tools
|
|
21
|
+
from .setup_cli import register_setup_command
|
|
22
|
+
from .stream import register_stream_tools
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
__version__ = "0.1.1"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register(ctx) -> None:
|
|
29
|
+
try:
|
|
30
|
+
_register_impl(ctx)
|
|
31
|
+
except Exception:
|
|
32
|
+
logger.exception("[clawclaw] plugin registration failed — "
|
|
33
|
+
"registering CLI fallback only")
|
|
34
|
+
try:
|
|
35
|
+
register_setup_command(ctx)
|
|
36
|
+
except Exception:
|
|
37
|
+
logger.exception("[clawclaw] CLI fallback registration also failed")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _register_impl(ctx) -> None:
|
|
41
|
+
cfg = Config.from_env()
|
|
42
|
+
invoker = resolve_ccl(cfg.ccl_path)
|
|
43
|
+
if invoker is None:
|
|
44
|
+
logger.warning("[clawclaw] ccl not found. Install with: npm i -g @myclaw163/clawclaw-cli")
|
|
45
|
+
logger.warning("[clawclaw] tools will error at call time until ccl is available.")
|
|
46
|
+
else:
|
|
47
|
+
logger.info("[clawclaw] using ccl: %s", invoker.display)
|
|
48
|
+
# Resolve workspace dir (priority: env var > ccl config > ~/.clawclaw)
|
|
49
|
+
if not cfg.workspace_dir:
|
|
50
|
+
ws = resolve_workspace(invoker)
|
|
51
|
+
if ws:
|
|
52
|
+
cfg = cfg.with_workspace(ws)
|
|
53
|
+
logger.info("[clawclaw] workspace from ccl: %s", ws)
|
|
54
|
+
else:
|
|
55
|
+
cfg = cfg.with_workspace(str(Path.home() / ".clawclaw"))
|
|
56
|
+
logger.info("[clawclaw] workspace default: %s", cfg.workspace_dir)
|
|
57
|
+
|
|
58
|
+
# 1. curated overrides FIRST (so auto-gen skips these names)
|
|
59
|
+
register_handwritten(ctx, invoker if invoker else _NullInvoker(), cfg)
|
|
60
|
+
# 2. auto-gen (no-op if ccl missing or schema fetch fails)
|
|
61
|
+
register_auto_tools(ctx, invoker, cfg)
|
|
62
|
+
# 3. stream tools + cleanup hooks
|
|
63
|
+
register_stream_tools(ctx, invoker, cfg)
|
|
64
|
+
# 4. setup CLI command (includes `hermes clawclaw setup` for skill install)
|
|
65
|
+
register_setup_command(ctx)
|
|
66
|
+
logger.info("[clawclaw] plugin registered")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _NullInvoker:
|
|
74
|
+
"""Placeholder so curated handlers can register even when ccl is absent;
|
|
75
|
+
they return a clear error at call time via the check_fn / runner."""
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
self.exe = ""
|
|
79
|
+
self.prefix_args: list[str] = []
|
|
80
|
+
|
|
81
|
+
def argv(self, extra): # pragma: no cover - never executed (check_fn gates)
|
|
82
|
+
return ["ccl", *extra]
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def display(self): # pragma: no cover
|
|
86
|
+
return "ccl (unresolved)"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Resolve the `ccl` (clawclaw-cli) executable and workspace dir."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
# Windows: suppress console-window flashes on every subprocess call.
|
|
12
|
+
# Three layers because each alone can fail depending on the call chain:
|
|
13
|
+
# CREATE_NO_WINDOW – prevents the system allocating a fresh console
|
|
14
|
+
# DETACHED_PROCESS – detaches from parent console entirely; children
|
|
15
|
+
# of a detached process can't inherit a console
|
|
16
|
+
# SW_HIDE – forces the window invisible via STARTUPINFO even
|
|
17
|
+
# if the process calls AllocConsole() internally
|
|
18
|
+
# Must be a factory: CreateProcess mutates STARTUPINFO, so each call needs
|
|
19
|
+
# a fresh copy to avoid cross-call races.
|
|
20
|
+
if sys.platform == "win32":
|
|
21
|
+
def _subprocess_kwargs():
|
|
22
|
+
si = subprocess.STARTUPINFO()
|
|
23
|
+
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
24
|
+
si.wShowWindow = subprocess.SW_HIDE
|
|
25
|
+
_flags = subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS
|
|
26
|
+
return {
|
|
27
|
+
"creationflags": _flags,
|
|
28
|
+
"startupinfo": si,
|
|
29
|
+
}
|
|
30
|
+
else:
|
|
31
|
+
def _subprocess_kwargs():
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Invoker:
|
|
37
|
+
exe: str
|
|
38
|
+
prefix_args: list[str] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
def argv(self, extra: list[str]) -> list[str]:
|
|
41
|
+
return [self.exe, *self.prefix_args, *extra]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def display(self) -> str:
|
|
45
|
+
return " ".join([self.exe, *self.prefix_args])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _node() -> str:
|
|
49
|
+
return shutil.which("node") or "node"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _mjs_from_shim(shim_path: str) -> str | None:
|
|
53
|
+
"""npm shim (<dir>/ccl.cmd) → <dir>/node_modules/@myclaw163/clawclaw-cli/bin/clawclaw-cli.mjs."""
|
|
54
|
+
d = os.path.dirname(shim_path)
|
|
55
|
+
cand = os.path.join(d, "node_modules", "@myclaw163", "clawclaw-cli", "bin", "clawclaw-cli.mjs")
|
|
56
|
+
return cand if os.path.isfile(cand) else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _invoker_for_path(path: str) -> Invoker | None:
|
|
60
|
+
if not path or not os.path.isfile(path):
|
|
61
|
+
return None
|
|
62
|
+
low = path.lower()
|
|
63
|
+
if low.endswith((".mjs", ".js")):
|
|
64
|
+
return Invoker(exe=_node(), prefix_args=[path])
|
|
65
|
+
if low.endswith((".cmd", ".bat", ".exe")):
|
|
66
|
+
mjs = _mjs_from_shim(path)
|
|
67
|
+
if mjs:
|
|
68
|
+
return Invoker(exe=_node(), prefix_args=[mjs])
|
|
69
|
+
if low.endswith(".exe"):
|
|
70
|
+
return Invoker(exe=path) # native binary, runnable directly
|
|
71
|
+
return None # .cmd/.bat with no resolvable .mjs — unusable without a shell
|
|
72
|
+
# POSIX: a plain executable (e.g. /usr/bin/ccl shell wrapper) runs directly.
|
|
73
|
+
return Invoker(exe=path)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_ccl(override: str | None = None) -> Invoker | None:
|
|
77
|
+
candidates: list[str] = []
|
|
78
|
+
if override:
|
|
79
|
+
candidates.append(override)
|
|
80
|
+
|
|
81
|
+
is_win = sys.platform == "win32"
|
|
82
|
+
exe_names = (
|
|
83
|
+
["ccl.cmd", "ccl.exe", "ccl", "clawclaw-cli.cmd", "clawclaw-cli.exe", "clawclaw-cli"]
|
|
84
|
+
if is_win
|
|
85
|
+
else ["ccl", "clawclaw-cli"]
|
|
86
|
+
)
|
|
87
|
+
for name in exe_names:
|
|
88
|
+
found = shutil.which(name)
|
|
89
|
+
if found:
|
|
90
|
+
candidates.append(found)
|
|
91
|
+
|
|
92
|
+
for c in candidates:
|
|
93
|
+
inv = _invoker_for_path(c)
|
|
94
|
+
if inv:
|
|
95
|
+
return inv
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_workspace(invoker: Invoker, timeout_ms: int = 5_000) -> str | None:
|
|
100
|
+
"""Query ccl for its configured workspace dir. Returns None on failure."""
|
|
101
|
+
try:
|
|
102
|
+
proc = subprocess.run(
|
|
103
|
+
invoker.argv(["config", "workspace"]),
|
|
104
|
+
capture_output=True,
|
|
105
|
+
text=True,
|
|
106
|
+
timeout=timeout_ms / 1000,
|
|
107
|
+
**_subprocess_kwargs(),
|
|
108
|
+
)
|
|
109
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
110
|
+
return None
|
|
111
|
+
if proc.returncode != 0:
|
|
112
|
+
return None
|
|
113
|
+
out = (proc.stdout or "").strip()
|
|
114
|
+
if not out:
|
|
115
|
+
return None
|
|
116
|
+
try:
|
|
117
|
+
data = json.loads(out)
|
|
118
|
+
if isinstance(data, dict) and "path" in data:
|
|
119
|
+
return data["path"]
|
|
120
|
+
if isinstance(data, str):
|
|
121
|
+
return data
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
return out
|
|
124
|
+
return out
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Env-var configuration. Defaults mirror STREAM_DEFAULTS from stream.ts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _int(name: str, default: int) -> int:
|
|
9
|
+
v = os.environ.get(name)
|
|
10
|
+
try:
|
|
11
|
+
return int(v) if v not in (None, "") else default
|
|
12
|
+
except ValueError:
|
|
13
|
+
return default
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _bool(name: str, default: bool) -> bool:
|
|
17
|
+
v = os.environ.get(name)
|
|
18
|
+
if v in (None, ""):
|
|
19
|
+
return default
|
|
20
|
+
return v.strip().lower() in ("1", "true", "yes", "on")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _str(name: str) -> str | None:
|
|
24
|
+
v = os.environ.get(name)
|
|
25
|
+
return v if v else None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Config:
|
|
30
|
+
ccl_path: str | None
|
|
31
|
+
workspace_dir: str | None # None = not yet resolved; ccl uses its own default
|
|
32
|
+
spawn_timeout_ms: int
|
|
33
|
+
stream_debounce_ms: int
|
|
34
|
+
stream_min_interval_ms: int
|
|
35
|
+
stream_max_batch_lines: int
|
|
36
|
+
stream_max_queue_lines: int
|
|
37
|
+
stream_max_instances: int
|
|
38
|
+
stream_auto_restart: bool
|
|
39
|
+
stream_max_restarts: int
|
|
40
|
+
stream_restart_backoff_base_ms: int
|
|
41
|
+
stream_restart_backoff_max_ms: int
|
|
42
|
+
stream_idle_timeout_ms: int
|
|
43
|
+
stream_emit_lifecycle: bool
|
|
44
|
+
stream_max_batch_age_ms: int
|
|
45
|
+
stream_verbose: bool
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_env(cls) -> "Config":
|
|
49
|
+
return cls(
|
|
50
|
+
ccl_path=_str("CLAWCLAW_CCL_PATH"),
|
|
51
|
+
workspace_dir=_str("CLAWCLAW_WORKSPACE_DIR"),
|
|
52
|
+
spawn_timeout_ms=_int("CLAWCLAW_SPAWN_TIMEOUT_MS", 30_000),
|
|
53
|
+
stream_debounce_ms=_int("CLAWCLAW_STREAM_DEBOUNCE_MS", 300),
|
|
54
|
+
stream_min_interval_ms=_int("CLAWCLAW_STREAM_MIN_INTERVAL_MS", 1_000),
|
|
55
|
+
stream_max_batch_lines=_int("CLAWCLAW_STREAM_MAX_BATCH_LINES", 150),
|
|
56
|
+
stream_max_queue_lines=_int("CLAWCLAW_STREAM_MAX_QUEUE_LINES", 1_000),
|
|
57
|
+
stream_max_instances=_int("CLAWCLAW_STREAM_MAX_INSTANCES", 8),
|
|
58
|
+
stream_auto_restart=_bool("CLAWCLAW_STREAM_AUTO_RESTART", True),
|
|
59
|
+
stream_max_restarts=_int("CLAWCLAW_STREAM_MAX_RESTARTS", 5),
|
|
60
|
+
stream_restart_backoff_base_ms=_int("CLAWCLAW_STREAM_RESTART_BACKOFF_BASE_MS", 1_000),
|
|
61
|
+
stream_restart_backoff_max_ms=_int("CLAWCLAW_STREAM_RESTART_BACKOFF_MAX_MS", 30_000),
|
|
62
|
+
stream_idle_timeout_ms=_int("CLAWCLAW_STREAM_IDLE_TIMEOUT_MS", 120_000),
|
|
63
|
+
stream_emit_lifecycle=_bool("CLAWCLAW_STREAM_EMIT_LIFECYCLE", True),
|
|
64
|
+
stream_max_batch_age_ms=_int("CLAWCLAW_STREAM_MAX_BATCH_AGE_MS", 3_000),
|
|
65
|
+
stream_verbose=_bool("CLAWCLAW_STREAM_VERBOSE", True),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def with_workspace(self, path: str) -> "Config":
|
|
69
|
+
return replace(self, workspace_dir=path)
|