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.
Files changed (27) hide show
  1. hermes_clawclaw-0.1.1/PKG-INFO +152 -0
  2. hermes_clawclaw-0.1.1/README.md +135 -0
  3. hermes_clawclaw-0.1.1/hermes_clawclaw/__init__.py +86 -0
  4. hermes_clawclaw-0.1.1/hermes_clawclaw/cli_resolve.py +124 -0
  5. hermes_clawclaw-0.1.1/hermes_clawclaw/config.py +69 -0
  6. hermes_clawclaw-0.1.1/hermes_clawclaw/handwritten.py +169 -0
  7. hermes_clawclaw-0.1.1/hermes_clawclaw/runner.py +51 -0
  8. hermes_clawclaw-0.1.1/hermes_clawclaw/schema_tools.py +163 -0
  9. hermes_clawclaw-0.1.1/hermes_clawclaw/setup_cli.py +99 -0
  10. hermes_clawclaw-0.1.1/hermes_clawclaw/stream.py +798 -0
  11. hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/PKG-INFO +152 -0
  12. hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/SOURCES.txt +25 -0
  13. hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/dependency_links.txt +1 -0
  14. hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/entry_points.txt +2 -0
  15. hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/requires.txt +3 -0
  16. hermes_clawclaw-0.1.1/hermes_clawclaw.egg-info/top_level.txt +1 -0
  17. hermes_clawclaw-0.1.1/pyproject.toml +33 -0
  18. hermes_clawclaw-0.1.1/setup.cfg +4 -0
  19. hermes_clawclaw-0.1.1/tests/test_cli_resolve.py +25 -0
  20. hermes_clawclaw-0.1.1/tests/test_config.py +26 -0
  21. hermes_clawclaw-0.1.1/tests/test_handwritten.py +51 -0
  22. hermes_clawclaw-0.1.1/tests/test_register.py +52 -0
  23. hermes_clawclaw-0.1.1/tests/test_runner.py +45 -0
  24. hermes_clawclaw-0.1.1/tests/test_schema_tools.py +49 -0
  25. hermes_clawclaw-0.1.1/tests/test_setup_cli.py +64 -0
  26. hermes_clawclaw-0.1.1/tests/test_stream_core.py +154 -0
  27. 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)