Spaces:
Build error
Build error
Commit ·
2c747d4
1
Parent(s): 5c747ec
feat(cli): support OpenClaw in Docker-native installs
Browse filesAdd host-managed OpenClaw wrap and unwrap flows to the Docker-native wrappers so the installed headroom script can configure the OpenClaw plugin on the host while keeping Headroom itself in Docker. Reuse hidden prepare-only hooks for OpenClaw config payloads, preserve existing plugin metadata on unwrap, and update the Docker-native and integration docs to reflect the supported flow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- README.md +1 -1
- docs/cli.md +5 -7
- docs/docker-install.md +6 -4
- docs/integration-guide.md +8 -2
- docs/typescript-sdk.md +1 -1
- headroom/cli/wrap.py +68 -18
- plugins/openclaw/README.md +2 -0
- scripts/install.ps1 +512 -3
- scripts/install.sh +462 -5
- tests/test_cli/test_wrap_bridge.py +54 -0
- tests/test_cli/test_wrap_openclaw.py +18 -0
README.md
CHANGED
|
@@ -153,7 +153,7 @@ headroom wrap codex --memory # Shares the same memory store
|
|
| 153 |
|
| 154 |
Headroom starts a proxy, points your tool at it, and compresses everything automatically. Add `--memory` for persistent memory that's shared across agents.
|
| 155 |
|
| 156 |
-
In Docker-native mode, Headroom still runs in Docker while wrapped tools run on the host. `wrap claude`, `wrap codex`, `wrap aider`,
|
| 157 |
|
| 158 |
### Multi-agent — SharedContext
|
| 159 |
|
|
|
|
| 153 |
|
| 154 |
Headroom starts a proxy, points your tool at it, and compresses everything automatically. Add `--memory` for persistent memory that's shared across agents.
|
| 155 |
|
| 156 |
+
In Docker-native mode, Headroom still runs in Docker while wrapped tools run on the host. `wrap claude`, `wrap codex`, `wrap aider`, `wrap cursor`, and OpenClaw plugin setup (`wrap openclaw` / `unwrap openclaw`) are host-managed through the installed wrapper.
|
| 157 |
|
| 158 |
### Multi-agent — SharedContext
|
| 159 |
|
docs/cli.md
CHANGED
|
@@ -32,8 +32,8 @@ This page is the authoritative reference for the **Python Headroom CLI** exposed
|
|
| 32 |
| `headroom wrap codex` | Start proxy and launch Codex CLI | **host-bridged** |
|
| 33 |
| `headroom wrap aider` | Start proxy and launch Aider | **host-bridged** |
|
| 34 |
| `headroom wrap cursor` | Start proxy and print Cursor config guidance | **host-bridged** |
|
| 35 |
-
| `headroom wrap openclaw` | Install and configure the OpenClaw plugin | **
|
| 36 |
-
| `headroom unwrap openclaw` | Disable the Headroom OpenClaw plugin | **
|
| 37 |
|
| 38 |
## Captured `--help` output
|
| 39 |
|
|
@@ -650,7 +650,7 @@ headroom wrap openclaw --plugin-path ./plugins/openclaw
|
|
| 650 |
| `--no-restart` | off | Do not restart the OpenClaw gateway |
|
| 651 |
| `--verbose`, `-v` | off | Verbose output |
|
| 652 |
|
| 653 |
-
Requires the `openclaw` binary on the host, and local-source mode may also require `npm`.
|
| 654 |
|
| 655 |
## `headroom unwrap`
|
| 656 |
|
|
@@ -678,8 +678,6 @@ Legend:
|
|
| 678 |
|
| 679 |
- **native in container** — the command runs entirely inside the Headroom container
|
| 680 |
- **host-bridged** — Headroom runs in Docker, but the wrapped external tool still runs on the host
|
| 681 |
-
- **unsupported** — use a native Python install of Headroom
|
| 682 |
-
|
| 683 |
| Command path | Python CLI | Docker-native wrapper | Parity |
|
| 684 |
|---|---|---|---|
|
| 685 |
| `headroom proxy` | native | native in container | full |
|
|
@@ -696,8 +694,8 @@ Legend:
|
|
| 696 |
| `headroom wrap codex` | native | host-bridged | partial |
|
| 697 |
| `headroom wrap aider` | native | host-bridged | partial |
|
| 698 |
| `headroom wrap cursor` | native | host-bridged | partial |
|
| 699 |
-
| `headroom wrap openclaw` | native |
|
| 700 |
-
| `headroom unwrap openclaw` | native |
|
| 701 |
|
| 702 |
For the Docker-native execution model itself, see [Docker-Native Install](docker-install.md).
|
| 703 |
|
|
|
|
| 32 |
| `headroom wrap codex` | Start proxy and launch Codex CLI | **host-bridged** |
|
| 33 |
| `headroom wrap aider` | Start proxy and launch Aider | **host-bridged** |
|
| 34 |
| `headroom wrap cursor` | Start proxy and print Cursor config guidance | **host-bridged** |
|
| 35 |
+
| `headroom wrap openclaw` | Install and configure the OpenClaw plugin | **host-bridged** |
|
| 36 |
+
| `headroom unwrap openclaw` | Disable the Headroom OpenClaw plugin | **host-bridged** |
|
| 37 |
|
| 38 |
## Captured `--help` output
|
| 39 |
|
|
|
|
| 650 |
| `--no-restart` | off | Do not restart the OpenClaw gateway |
|
| 651 |
| `--verbose`, `-v` | off | Verbose output |
|
| 652 |
|
| 653 |
+
Requires the `openclaw` binary on the host, and local-source mode may also require `npm`. In Docker-native mode, the installed host wrapper drives the host `openclaw` CLI while the plugin auto-starts the host `headroom` wrapper from `PATH`.
|
| 654 |
|
| 655 |
## `headroom unwrap`
|
| 656 |
|
|
|
|
| 678 |
|
| 679 |
- **native in container** — the command runs entirely inside the Headroom container
|
| 680 |
- **host-bridged** — Headroom runs in Docker, but the wrapped external tool still runs on the host
|
|
|
|
|
|
|
| 681 |
| Command path | Python CLI | Docker-native wrapper | Parity |
|
| 682 |
|---|---|---|---|
|
| 683 |
| `headroom proxy` | native | native in container | full |
|
|
|
|
| 694 |
| `headroom wrap codex` | native | host-bridged | partial |
|
| 695 |
| `headroom wrap aider` | native | host-bridged | partial |
|
| 696 |
| `headroom wrap cursor` | native | host-bridged | partial |
|
| 697 |
+
| `headroom wrap openclaw` | native | host-bridged | partial |
|
| 698 |
+
| `headroom unwrap openclaw` | native | host-bridged | partial |
|
| 699 |
|
| 700 |
For the Docker-native execution model itself, see [Docker-Native Install](docker-install.md).
|
| 701 |
|
docs/docker-install.md
CHANGED
|
@@ -71,13 +71,15 @@ Supported host wrap flows:
|
|
| 71 |
- `headroom wrap codex`
|
| 72 |
- `headroom wrap aider`
|
| 73 |
- `headroom wrap cursor`
|
| 74 |
-
|
| 75 |
-
Not yet supported in Docker-native mode:
|
| 76 |
-
|
| 77 |
- `headroom wrap openclaw`
|
| 78 |
- `headroom unwrap openclaw`
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
## Docker Compose support
|
| 83 |
|
|
|
|
| 71 |
- `headroom wrap codex`
|
| 72 |
- `headroom wrap aider`
|
| 73 |
- `headroom wrap cursor`
|
|
|
|
|
|
|
|
|
|
| 74 |
- `headroom wrap openclaw`
|
| 75 |
- `headroom unwrap openclaw`
|
| 76 |
|
| 77 |
+
OpenClaw remains host-native in Docker-native mode:
|
| 78 |
+
|
| 79 |
+
- the host must already have the `openclaw` CLI installed
|
| 80 |
+
- `headroom wrap openclaw` installs/configures the Headroom plugin through the host `openclaw` CLI
|
| 81 |
+
- plugin auto-start still launches the installed host `headroom` wrapper from `PATH`, which then runs Headroom in Docker
|
| 82 |
+
- local plugin source mode (`--plugin-path`) is also supported, but it may require host `npm` when build steps are needed
|
| 83 |
|
| 84 |
## Docker Compose support
|
| 85 |
|
docs/integration-guide.md
CHANGED
|
@@ -270,8 +270,7 @@ See the [TypeScript SDK Guide](typescript-sdk.md) for full documentation includi
|
|
| 270 |
Context compression plugin for [OpenClaw](https://github.com/openclaw/openclaw) agents.
|
| 271 |
|
| 272 |
```bash
|
| 273 |
-
|
| 274 |
-
openclaw plugins install headroom-openclaw
|
| 275 |
```
|
| 276 |
|
| 277 |
Configure as context engine:
|
|
@@ -279,6 +278,13 @@ Configure as context engine:
|
|
| 279 |
{ "plugins": { "slots": { "contextEngine": "headroom" } } }
|
| 280 |
```
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
The plugin auto-detects a running Headroom proxy or starts one. Compression happens in `assemble()` — zero changes to the agent's behavior.
|
| 283 |
|
| 284 |
See the [OpenClaw plugin documentation](https://github.com/chopratejas/headroom/tree/main/plugins/openclaw) for full setup.
|
|
|
|
| 270 |
Context compression plugin for [OpenClaw](https://github.com/openclaw/openclaw) agents.
|
| 271 |
|
| 272 |
```bash
|
| 273 |
+
headroom wrap openclaw
|
|
|
|
| 274 |
```
|
| 275 |
|
| 276 |
Configure as context engine:
|
|
|
|
| 278 |
{ "plugins": { "slots": { "contextEngine": "headroom" } } }
|
| 279 |
```
|
| 280 |
|
| 281 |
+
Manual install remains available when you are not using the CLI wrapper:
|
| 282 |
+
|
| 283 |
+
```bash
|
| 284 |
+
pip install "headroom-ai[proxy]"
|
| 285 |
+
openclaw plugins install --dangerously-force-unsafe-install headroom-ai/openclaw
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
The plugin auto-detects a running Headroom proxy or starts one. Compression happens in `assemble()` — zero changes to the agent's behavior.
|
| 289 |
|
| 290 |
See the [OpenClaw plugin documentation](https://github.com/chopratejas/headroom/tree/main/plugins/openclaw) for full setup.
|
docs/typescript-sdk.md
CHANGED
|
@@ -200,7 +200,7 @@ The `headroom-ai` package has no runtime dependencies. Framework SDKs (Vercel AI
|
|
| 200 |
|
| 201 |
## OpenClaw Plugin
|
| 202 |
|
| 203 |
-
The TypeScript SDK powers the [`headroom-openclaw`](https://www.npmjs.com/package/headroom-openclaw) plugin for [OpenClaw](https://github.com/openclaw/openclaw) agents. The plugin uses `HeadroomClient` internally to compress context during the `assemble()` lifecycle hook.
|
| 204 |
|
| 205 |
## Comparison with Python SDK
|
| 206 |
|
|
|
|
| 200 |
|
| 201 |
## OpenClaw Plugin
|
| 202 |
|
| 203 |
+
The TypeScript SDK powers the [`headroom-openclaw`](https://www.npmjs.com/package/headroom-openclaw) plugin for [OpenClaw](https://github.com/openclaw/openclaw) agents. The plugin uses `HeadroomClient` internally to compress context during the `assemble()` lifecycle hook. The preferred install flow is `headroom wrap openclaw`; the direct plugin command is `openclaw plugins install --dangerously-force-unsafe-install headroom-ai/openclaw`. See the [plugin source](https://github.com/chopratejas/headroom/tree/main/plugins/openclaw) for details.
|
| 204 |
|
| 205 |
## Comparison with Python SDK
|
| 206 |
|
headroom/cli/wrap.py
CHANGED
|
@@ -549,6 +549,17 @@ def _read_openclaw_config_value(openclaw_bin: str, path: str) -> Any | None:
|
|
| 549 |
return output
|
| 550 |
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
def _build_openclaw_plugin_entry(
|
| 553 |
*,
|
| 554 |
existing_entry: Any,
|
|
@@ -583,6 +594,28 @@ def _build_openclaw_plugin_entry(
|
|
| 583 |
}
|
| 584 |
|
| 585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
def _write_openclaw_plugin_entry(openclaw_bin: str, entry: dict[str, object]) -> None:
|
| 587 |
"""Persist the Headroom plugin config entry."""
|
| 588 |
_run_checked(
|
|
@@ -1132,6 +1165,8 @@ def cursor(
|
|
| 1132 |
help="Do not restart OpenClaw gateway at the end",
|
| 1133 |
)
|
| 1134 |
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
|
|
|
|
|
|
| 1135 |
def openclaw(
|
| 1136 |
plugin_path: Path | None,
|
| 1137 |
plugin_spec: str,
|
|
@@ -1144,6 +1179,8 @@ def openclaw(
|
|
| 1144 |
no_auto_start: bool,
|
| 1145 |
no_restart: bool,
|
| 1146 |
verbose: bool,
|
|
|
|
|
|
|
| 1147 |
) -> None:
|
| 1148 |
"""Install and configure Headroom OpenClaw plugin in one command.
|
| 1149 |
|
|
@@ -1160,6 +1197,19 @@ def openclaw(
|
|
| 1160 |
headroom wrap openclaw
|
| 1161 |
headroom wrap openclaw --plugin-path C:\\git\\headroom\\plugins\\openclaw
|
| 1162 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1163 |
openclaw_bin = shutil.which("openclaw")
|
| 1164 |
if not openclaw_bin:
|
| 1165 |
raise click.ClickException("'openclaw' not found in PATH. Install OpenClaw CLI first.")
|
|
@@ -1304,8 +1354,24 @@ def openclaw(
|
|
| 1304 |
@unwrap.command("openclaw")
|
| 1305 |
@click.option("--no-restart", is_flag=True, help="Do not restart OpenClaw gateway at the end")
|
| 1306 |
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
| 1307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1308 |
"""Disable the Headroom OpenClaw plugin and restore the legacy engine slot."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1309 |
openclaw_bin = shutil.which("openclaw")
|
| 1310 |
if not openclaw_bin:
|
| 1311 |
raise click.ClickException("'openclaw' not found in PATH. Install OpenClaw CLI first.")
|
|
@@ -1318,23 +1384,7 @@ def unwrap_openclaw(no_restart: bool, verbose: bool) -> None:
|
|
| 1318 |
click.echo(" Disabling Headroom plugin and removing engine mapping...")
|
| 1319 |
|
| 1320 |
existing_entry = _read_openclaw_config_value(openclaw_bin, "plugins.entries.headroom")
|
| 1321 |
-
|
| 1322 |
-
if isinstance(existing_entry, dict) and isinstance(existing_entry.get("config"), dict):
|
| 1323 |
-
existing_config = {
|
| 1324 |
-
key: value
|
| 1325 |
-
for key, value in existing_entry["config"].items()
|
| 1326 |
-
if key
|
| 1327 |
-
not in {
|
| 1328 |
-
"gatewayProviderIds",
|
| 1329 |
-
"proxyUrl",
|
| 1330 |
-
"proxyPort",
|
| 1331 |
-
"autoStart",
|
| 1332 |
-
"startupTimeoutMs",
|
| 1333 |
-
"pythonPath",
|
| 1334 |
-
}
|
| 1335 |
-
}
|
| 1336 |
-
|
| 1337 |
-
entry = {"enabled": False, "config": existing_config}
|
| 1338 |
_write_openclaw_plugin_entry(openclaw_bin, entry)
|
| 1339 |
_set_openclaw_context_engine_slot(openclaw_bin, "legacy")
|
| 1340 |
_run_checked(
|
|
|
|
| 549 |
return output
|
| 550 |
|
| 551 |
|
| 552 |
+
def _decode_openclaw_entry_json(raw_value: str | None) -> Any | None:
|
| 553 |
+
"""Decode a JSON payload captured from `openclaw config get` when available."""
|
| 554 |
+
if not raw_value:
|
| 555 |
+
return None
|
| 556 |
+
|
| 557 |
+
try:
|
| 558 |
+
return json.loads(raw_value)
|
| 559 |
+
except json.JSONDecodeError:
|
| 560 |
+
return raw_value
|
| 561 |
+
|
| 562 |
+
|
| 563 |
def _build_openclaw_plugin_entry(
|
| 564 |
*,
|
| 565 |
existing_entry: Any,
|
|
|
|
| 594 |
}
|
| 595 |
|
| 596 |
|
| 597 |
+
def _build_openclaw_unwrap_entry(existing_entry: Any) -> dict[str, object]:
|
| 598 |
+
"""Disable the managed plugin while preserving unrelated user config."""
|
| 599 |
+
base_entry = existing_entry if isinstance(existing_entry, dict) else {}
|
| 600 |
+
existing_config = {}
|
| 601 |
+
if isinstance(existing_entry, dict) and isinstance(existing_entry.get("config"), dict):
|
| 602 |
+
existing_config = {
|
| 603 |
+
key: value
|
| 604 |
+
for key, value in existing_entry["config"].items()
|
| 605 |
+
if key
|
| 606 |
+
not in {
|
| 607 |
+
"gatewayProviderIds",
|
| 608 |
+
"proxyUrl",
|
| 609 |
+
"proxyPort",
|
| 610 |
+
"autoStart",
|
| 611 |
+
"startupTimeoutMs",
|
| 612 |
+
"pythonPath",
|
| 613 |
+
}
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
return {**base_entry, "enabled": False, "config": existing_config}
|
| 617 |
+
|
| 618 |
+
|
| 619 |
def _write_openclaw_plugin_entry(openclaw_bin: str, entry: dict[str, object]) -> None:
|
| 620 |
"""Persist the Headroom plugin config entry."""
|
| 621 |
_run_checked(
|
|
|
|
| 1165 |
help="Do not restart OpenClaw gateway at the end",
|
| 1166 |
)
|
| 1167 |
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
| 1168 |
+
@click.option("--prepare-only", is_flag=True, hidden=True)
|
| 1169 |
+
@click.option("--existing-entry-json", default=None, hidden=True)
|
| 1170 |
def openclaw(
|
| 1171 |
plugin_path: Path | None,
|
| 1172 |
plugin_spec: str,
|
|
|
|
| 1179 |
no_auto_start: bool,
|
| 1180 |
no_restart: bool,
|
| 1181 |
verbose: bool,
|
| 1182 |
+
prepare_only: bool,
|
| 1183 |
+
existing_entry_json: str | None,
|
| 1184 |
) -> None:
|
| 1185 |
"""Install and configure Headroom OpenClaw plugin in one command.
|
| 1186 |
|
|
|
|
| 1197 |
headroom wrap openclaw
|
| 1198 |
headroom wrap openclaw --plugin-path C:\\git\\headroom\\plugins\\openclaw
|
| 1199 |
"""
|
| 1200 |
+
if prepare_only:
|
| 1201 |
+
entry = _build_openclaw_plugin_entry(
|
| 1202 |
+
existing_entry=_decode_openclaw_entry_json(existing_entry_json),
|
| 1203 |
+
proxy_port=proxy_port,
|
| 1204 |
+
startup_timeout_ms=startup_timeout_ms,
|
| 1205 |
+
python_path=python_path,
|
| 1206 |
+
no_auto_start=no_auto_start,
|
| 1207 |
+
gateway_provider_ids=gateway_provider_ids,
|
| 1208 |
+
enabled=True,
|
| 1209 |
+
)
|
| 1210 |
+
click.echo(json.dumps(entry, separators=(",", ":")))
|
| 1211 |
+
return
|
| 1212 |
+
|
| 1213 |
openclaw_bin = shutil.which("openclaw")
|
| 1214 |
if not openclaw_bin:
|
| 1215 |
raise click.ClickException("'openclaw' not found in PATH. Install OpenClaw CLI first.")
|
|
|
|
| 1354 |
@unwrap.command("openclaw")
|
| 1355 |
@click.option("--no-restart", is_flag=True, help="Do not restart OpenClaw gateway at the end")
|
| 1356 |
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
| 1357 |
+
@click.option("--prepare-only", is_flag=True, hidden=True)
|
| 1358 |
+
@click.option("--existing-entry-json", default=None, hidden=True)
|
| 1359 |
+
def unwrap_openclaw(
|
| 1360 |
+
no_restart: bool,
|
| 1361 |
+
verbose: bool,
|
| 1362 |
+
prepare_only: bool,
|
| 1363 |
+
existing_entry_json: str | None,
|
| 1364 |
+
) -> None:
|
| 1365 |
"""Disable the Headroom OpenClaw plugin and restore the legacy engine slot."""
|
| 1366 |
+
if prepare_only:
|
| 1367 |
+
click.echo(
|
| 1368 |
+
json.dumps(
|
| 1369 |
+
_build_openclaw_unwrap_entry(_decode_openclaw_entry_json(existing_entry_json)),
|
| 1370 |
+
separators=(",", ":"),
|
| 1371 |
+
)
|
| 1372 |
+
)
|
| 1373 |
+
return
|
| 1374 |
+
|
| 1375 |
openclaw_bin = shutil.which("openclaw")
|
| 1376 |
if not openclaw_bin:
|
| 1377 |
raise click.ClickException("'openclaw' not found in PATH. Install OpenClaw CLI first.")
|
|
|
|
| 1384 |
click.echo(" Disabling Headroom plugin and removing engine mapping...")
|
| 1385 |
|
| 1386 |
existing_entry = _read_openclaw_config_value(openclaw_bin, "plugins.entries.headroom")
|
| 1387 |
+
entry = _build_openclaw_unwrap_entry(existing_entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1388 |
_write_openclaw_plugin_entry(openclaw_bin, entry)
|
| 1389 |
_set_openclaw_context_engine_slot(openclaw_bin, "legacy")
|
| 1390 |
_run_checked(
|
plugins/openclaw/README.md
CHANGED
|
@@ -150,6 +150,8 @@ When `proxyUrl` points to localhost (or is omitted), the plugin will auto-start
|
|
| 150 |
|
| 151 |
If `pythonPath` is set, it is tried first in the Python fallback step.
|
| 152 |
|
|
|
|
|
|
|
| 153 |
### Remote proxy (connect-only)
|
| 154 |
|
| 155 |
Point `proxyUrl` to any reachable Headroom instance:
|
|
|
|
| 150 |
|
| 151 |
If `pythonPath` is set, it is tried first in the Python fallback step.
|
| 152 |
|
| 153 |
+
Docker-native Headroom installs intentionally leave `pythonPath` unset so this launcher order prefers the installed host `headroom` wrapper on `PATH`, which then runs Headroom in Docker.
|
| 154 |
+
|
| 155 |
### Remote proxy (connect-only)
|
| 156 |
|
| 157 |
Point `proxyUrl` to any reachable Headroom instance:
|
scripts/install.ps1
CHANGED
|
@@ -265,6 +265,496 @@ function Invoke-WithTemporaryEnv {
|
|
| 265 |
}
|
| 266 |
}
|
| 267 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
function Parse-WrapArgs {
|
| 269 |
param([string[]]$Arguments)
|
| 270 |
|
|
@@ -421,11 +911,23 @@ if ($args.Count -eq 0) {
|
|
| 421 |
switch ($args[0]) {
|
| 422 |
'wrap' {
|
| 423 |
if ($args.Count -lt 2) {
|
| 424 |
-
Fail 'Usage: headroom wrap <claude|codex|aider|cursor> [...]'
|
| 425 |
}
|
| 426 |
|
| 427 |
$tool = $args[1]
|
| 428 |
$wrapArgs = if ($args.Count -gt 2) { $args[2..($args.Count - 1)] } else { @() }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
$parsed = Parse-WrapArgs -Arguments $wrapArgs
|
| 430 |
$proxyArgs = New-Object System.Collections.Generic.List[string]
|
| 431 |
if ($parsed.Learn) { $proxyArgs.Add('--learn') }
|
|
@@ -438,7 +940,6 @@ switch ($args[0]) {
|
|
| 438 |
'codex' { }
|
| 439 |
'aider' { }
|
| 440 |
'cursor' { }
|
| 441 |
-
'openclaw' { Fail "Docker-native install does not support 'headroom wrap openclaw' yet. Use a native Headroom install for OpenClaw plugin management." }
|
| 442 |
default { Fail "Unsupported wrap target: $tool" }
|
| 443 |
}
|
| 444 |
|
|
@@ -490,7 +991,15 @@ switch ($args[0]) {
|
|
| 490 |
}
|
| 491 |
'unwrap' {
|
| 492 |
if ($args.Count -ge 2 -and $args[1] -eq 'openclaw') {
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
}
|
| 495 |
Invoke-HeadroomDocker -Arguments $args
|
| 496 |
}
|
|
|
|
| 265 |
}
|
| 266 |
}
|
| 267 |
|
| 268 |
+
function Test-HelpFlag {
|
| 269 |
+
param([string[]]$Arguments)
|
| 270 |
+
|
| 271 |
+
foreach ($arg in $Arguments) {
|
| 272 |
+
if ($arg -eq '--help' -or $arg -eq '-?') {
|
| 273 |
+
return $true
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
return $false
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
function Parse-OpenClawWrapArgs {
|
| 281 |
+
param([string[]]$Arguments)
|
| 282 |
+
|
| 283 |
+
$gatewayProviderIds = New-Object System.Collections.Generic.List[string]
|
| 284 |
+
$pluginPath = $null
|
| 285 |
+
$pluginSpec = 'headroom-ai/openclaw'
|
| 286 |
+
$skipBuild = $false
|
| 287 |
+
$copy = $false
|
| 288 |
+
$proxyPort = 8787
|
| 289 |
+
$startupTimeoutMs = 20000
|
| 290 |
+
$pythonPath = $null
|
| 291 |
+
$noAutoStart = $false
|
| 292 |
+
$noRestart = $false
|
| 293 |
+
$verbose = $false
|
| 294 |
+
|
| 295 |
+
$i = 0
|
| 296 |
+
while ($i -lt $Arguments.Count) {
|
| 297 |
+
$arg = $Arguments[$i]
|
| 298 |
+
switch -Regex ($arg) {
|
| 299 |
+
'^--plugin-path$' {
|
| 300 |
+
$pluginPath = $Arguments[$i + 1]
|
| 301 |
+
$i += 2
|
| 302 |
+
continue
|
| 303 |
+
}
|
| 304 |
+
'^--plugin-path=' {
|
| 305 |
+
$pluginPath = $arg -replace '^--plugin-path=', ''
|
| 306 |
+
$i += 1
|
| 307 |
+
continue
|
| 308 |
+
}
|
| 309 |
+
'^--plugin-spec$' {
|
| 310 |
+
$pluginSpec = $Arguments[$i + 1]
|
| 311 |
+
$i += 2
|
| 312 |
+
continue
|
| 313 |
+
}
|
| 314 |
+
'^--plugin-spec=' {
|
| 315 |
+
$pluginSpec = $arg -replace '^--plugin-spec=', ''
|
| 316 |
+
$i += 1
|
| 317 |
+
continue
|
| 318 |
+
}
|
| 319 |
+
'^--skip-build$' {
|
| 320 |
+
$skipBuild = $true
|
| 321 |
+
$i += 1
|
| 322 |
+
continue
|
| 323 |
+
}
|
| 324 |
+
'^--copy$' {
|
| 325 |
+
$copy = $true
|
| 326 |
+
$i += 1
|
| 327 |
+
continue
|
| 328 |
+
}
|
| 329 |
+
'^--proxy-port$' {
|
| 330 |
+
$proxyPort = [int]$Arguments[$i + 1]
|
| 331 |
+
$i += 2
|
| 332 |
+
continue
|
| 333 |
+
}
|
| 334 |
+
'^--proxy-port=' {
|
| 335 |
+
$proxyPort = [int]($arg -replace '^--proxy-port=', '')
|
| 336 |
+
$i += 1
|
| 337 |
+
continue
|
| 338 |
+
}
|
| 339 |
+
'^--startup-timeout-ms$' {
|
| 340 |
+
$startupTimeoutMs = [int]$Arguments[$i + 1]
|
| 341 |
+
$i += 2
|
| 342 |
+
continue
|
| 343 |
+
}
|
| 344 |
+
'^--startup-timeout-ms=' {
|
| 345 |
+
$startupTimeoutMs = [int]($arg -replace '^--startup-timeout-ms=', '')
|
| 346 |
+
$i += 1
|
| 347 |
+
continue
|
| 348 |
+
}
|
| 349 |
+
'^--gateway-provider-id$' {
|
| 350 |
+
$gatewayProviderIds.Add($Arguments[$i + 1])
|
| 351 |
+
$i += 2
|
| 352 |
+
continue
|
| 353 |
+
}
|
| 354 |
+
'^--gateway-provider-id=' {
|
| 355 |
+
$gatewayProviderIds.Add($arg -replace '^--gateway-provider-id=', '')
|
| 356 |
+
$i += 1
|
| 357 |
+
continue
|
| 358 |
+
}
|
| 359 |
+
'^--python-path$' {
|
| 360 |
+
$pythonPath = $Arguments[$i + 1]
|
| 361 |
+
$i += 2
|
| 362 |
+
continue
|
| 363 |
+
}
|
| 364 |
+
'^--python-path=' {
|
| 365 |
+
$pythonPath = $arg -replace '^--python-path=', ''
|
| 366 |
+
$i += 1
|
| 367 |
+
continue
|
| 368 |
+
}
|
| 369 |
+
'^--no-auto-start$' {
|
| 370 |
+
$noAutoStart = $true
|
| 371 |
+
$i += 1
|
| 372 |
+
continue
|
| 373 |
+
}
|
| 374 |
+
'^--no-restart$' {
|
| 375 |
+
$noRestart = $true
|
| 376 |
+
$i += 1
|
| 377 |
+
continue
|
| 378 |
+
}
|
| 379 |
+
'^--verbose$|^-v$' {
|
| 380 |
+
$verbose = $true
|
| 381 |
+
$i += 1
|
| 382 |
+
continue
|
| 383 |
+
}
|
| 384 |
+
default {
|
| 385 |
+
Fail "Unsupported option for 'headroom wrap openclaw': $arg"
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
[pscustomobject]@{
|
| 391 |
+
PluginPath = $pluginPath
|
| 392 |
+
PluginSpec = $pluginSpec
|
| 393 |
+
SkipBuild = $skipBuild
|
| 394 |
+
Copy = $copy
|
| 395 |
+
ProxyPort = $proxyPort
|
| 396 |
+
StartupTimeoutMs = $startupTimeoutMs
|
| 397 |
+
GatewayProviderIds = $gatewayProviderIds.ToArray()
|
| 398 |
+
PythonPath = $pythonPath
|
| 399 |
+
NoAutoStart = $noAutoStart
|
| 400 |
+
NoRestart = $noRestart
|
| 401 |
+
Verbose = $verbose
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
function Parse-OpenClawUnwrapArgs {
|
| 406 |
+
param([string[]]$Arguments)
|
| 407 |
+
|
| 408 |
+
$noRestart = $false
|
| 409 |
+
$verbose = $false
|
| 410 |
+
$i = 0
|
| 411 |
+
while ($i -lt $Arguments.Count) {
|
| 412 |
+
$arg = $Arguments[$i]
|
| 413 |
+
switch -Regex ($arg) {
|
| 414 |
+
'^--no-restart$' {
|
| 415 |
+
$noRestart = $true
|
| 416 |
+
$i += 1
|
| 417 |
+
continue
|
| 418 |
+
}
|
| 419 |
+
'^--verbose$|^-v$' {
|
| 420 |
+
$verbose = $true
|
| 421 |
+
$i += 1
|
| 422 |
+
continue
|
| 423 |
+
}
|
| 424 |
+
default {
|
| 425 |
+
Fail "Unsupported option for 'headroom unwrap openclaw': $arg"
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
[pscustomobject]@{
|
| 431 |
+
NoRestart = $noRestart
|
| 432 |
+
Verbose = $verbose
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
function Invoke-CapturedCommand {
|
| 437 |
+
param(
|
| 438 |
+
[string]$Action,
|
| 439 |
+
[string]$Command,
|
| 440 |
+
[string[]]$Arguments,
|
| 441 |
+
[string]$WorkingDirectory
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
$previous = $null
|
| 445 |
+
try {
|
| 446 |
+
if ($WorkingDirectory) {
|
| 447 |
+
$previous = Get-Location
|
| 448 |
+
Set-Location $WorkingDirectory
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
$output = (& $Command @Arguments 2>&1 | Out-String).Trim()
|
| 452 |
+
$exitCode = $LASTEXITCODE
|
| 453 |
+
} finally {
|
| 454 |
+
if ($previous) {
|
| 455 |
+
Set-Location $previous
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
if ($exitCode -ne 0) {
|
| 460 |
+
if (-not $output) {
|
| 461 |
+
$output = "exit code $exitCode"
|
| 462 |
+
}
|
| 463 |
+
Fail "$Action failed: $output"
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
return $output
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
function Get-OpenClawExistingEntryJson {
|
| 470 |
+
$output = (& openclaw config get plugins.entries.headroom 2>$null | Out-String).Trim()
|
| 471 |
+
if ($LASTEXITCODE -ne 0) {
|
| 472 |
+
return $null
|
| 473 |
+
}
|
| 474 |
+
return $output
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function Invoke-OpenClawPrepareEntryJson {
|
| 478 |
+
param(
|
| 479 |
+
[string]$ExistingEntryJson,
|
| 480 |
+
[pscustomobject]$Parsed
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
$dockerArgs = New-Object System.Collections.Generic.List[string]
|
| 484 |
+
$dockerArgs.AddRange(@('run','--rm'))
|
| 485 |
+
$dockerArgs.AddRange((Get-SharedDockerArgs))
|
| 486 |
+
$dockerArgs.Add('--entrypoint')
|
| 487 |
+
$dockerArgs.Add('headroom')
|
| 488 |
+
$dockerArgs.Add($HeadroomImage)
|
| 489 |
+
$dockerArgs.AddRange(@('wrap','openclaw','--prepare-only','--proxy-port',"$($Parsed.ProxyPort)",'--startup-timeout-ms',"$($Parsed.StartupTimeoutMs)"))
|
| 490 |
+
if ($ExistingEntryJson) {
|
| 491 |
+
$dockerArgs.Add('--existing-entry-json')
|
| 492 |
+
$dockerArgs.Add($ExistingEntryJson)
|
| 493 |
+
}
|
| 494 |
+
if ($Parsed.PythonPath) {
|
| 495 |
+
$dockerArgs.Add('--python-path')
|
| 496 |
+
$dockerArgs.Add($Parsed.PythonPath)
|
| 497 |
+
}
|
| 498 |
+
if ($Parsed.NoAutoStart) {
|
| 499 |
+
$dockerArgs.Add('--no-auto-start')
|
| 500 |
+
}
|
| 501 |
+
foreach ($providerId in $Parsed.GatewayProviderIds) {
|
| 502 |
+
$dockerArgs.Add('--gateway-provider-id')
|
| 503 |
+
$dockerArgs.Add($providerId)
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
$output = (& docker @dockerArgs 2>&1 | Out-String).Trim()
|
| 507 |
+
if ($LASTEXITCODE -ne 0) {
|
| 508 |
+
Fail "Failed to prepare docker-native OpenClaw config: $output"
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
return $output
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
function Invoke-OpenClawPrepareUnwrapEntryJson {
|
| 515 |
+
param([string]$ExistingEntryJson)
|
| 516 |
+
|
| 517 |
+
$dockerArgs = New-Object System.Collections.Generic.List[string]
|
| 518 |
+
$dockerArgs.AddRange(@('run','--rm'))
|
| 519 |
+
$dockerArgs.AddRange((Get-SharedDockerArgs))
|
| 520 |
+
$dockerArgs.Add('--entrypoint')
|
| 521 |
+
$dockerArgs.Add('headroom')
|
| 522 |
+
$dockerArgs.Add($HeadroomImage)
|
| 523 |
+
$dockerArgs.AddRange(@('unwrap','openclaw','--prepare-only'))
|
| 524 |
+
if ($ExistingEntryJson) {
|
| 525 |
+
$dockerArgs.Add('--existing-entry-json')
|
| 526 |
+
$dockerArgs.Add($ExistingEntryJson)
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
$output = (& docker @dockerArgs 2>&1 | Out-String).Trim()
|
| 530 |
+
if ($LASTEXITCODE -ne 0) {
|
| 531 |
+
Fail "Failed to prepare docker-native OpenClaw unwrap config: $output"
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
return $output
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
function Resolve-OpenClawExtensionsDir {
|
| 538 |
+
$configOutput = Invoke-CapturedCommand -Action 'openclaw config file' -Command 'openclaw' -Arguments @('config','file')
|
| 539 |
+
$configPath = ($configOutput -split "`r?`n")[-1].Trim()
|
| 540 |
+
if (-not $configPath) {
|
| 541 |
+
Fail 'Unable to resolve OpenClaw config path.'
|
| 542 |
+
}
|
| 543 |
+
return (Join-Path (Split-Path -Parent $configPath) 'extensions')
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
function Copy-OpenClawPluginIntoExtensions {
|
| 547 |
+
param([string]$PluginPath)
|
| 548 |
+
|
| 549 |
+
$distDir = Join-Path $PluginPath 'dist'
|
| 550 |
+
$hookShimDir = Join-Path $PluginPath 'hook-shim'
|
| 551 |
+
if (-not (Test-Path $distDir)) {
|
| 552 |
+
Fail "Plugin dist folder missing at $distDir. Build the plugin first."
|
| 553 |
+
}
|
| 554 |
+
if (-not (Test-Path $hookShimDir)) {
|
| 555 |
+
Fail "Plugin hook-shim folder missing at $hookShimDir. Build the plugin first."
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
$extensionsDir = Resolve-OpenClawExtensionsDir
|
| 559 |
+
$targetDir = Join-Path $extensionsDir 'headroom'
|
| 560 |
+
$targetDist = Join-Path $targetDir 'dist'
|
| 561 |
+
$targetHookShim = Join-Path $targetDir 'hook-shim'
|
| 562 |
+
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
|
| 563 |
+
if (Test-Path $targetDist) { Remove-Item -Recurse -Force $targetDist }
|
| 564 |
+
if (Test-Path $targetHookShim) { Remove-Item -Recurse -Force $targetHookShim }
|
| 565 |
+
Copy-Item -Recurse -Force $distDir $targetDist
|
| 566 |
+
Copy-Item -Recurse -Force $hookShimDir $targetHookShim
|
| 567 |
+
|
| 568 |
+
foreach ($fileName in @('openclaw.plugin.json','package.json','README.md')) {
|
| 569 |
+
$source = Join-Path $PluginPath $fileName
|
| 570 |
+
if (Test-Path $source) {
|
| 571 |
+
Copy-Item -Force $source (Join-Path $targetDir $fileName)
|
| 572 |
+
}
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
return $targetDir
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
function Install-OpenClawPlugin {
|
| 579 |
+
param([pscustomobject]$Parsed)
|
| 580 |
+
|
| 581 |
+
if ($Parsed.PluginPath) {
|
| 582 |
+
if (-not (Test-Path $Parsed.PluginPath)) {
|
| 583 |
+
Fail "Plugin path not found: $($Parsed.PluginPath)."
|
| 584 |
+
}
|
| 585 |
+
if (-not (Test-Path (Join-Path $Parsed.PluginPath 'package.json'))) {
|
| 586 |
+
Fail "Invalid plugin path (missing package.json): $($Parsed.PluginPath)"
|
| 587 |
+
}
|
| 588 |
+
if (-not (Test-Path (Join-Path $Parsed.PluginPath 'openclaw.plugin.json'))) {
|
| 589 |
+
Fail "Invalid plugin path (missing openclaw.plugin.json): $($Parsed.PluginPath)"
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
if ($Parsed.PluginPath -and -not $Parsed.SkipBuild) {
|
| 594 |
+
Require-Command npm
|
| 595 |
+
Write-Host ' Building OpenClaw plugin (npm install + npm run build)...'
|
| 596 |
+
[void](Invoke-CapturedCommand -Action 'npm install' -Command 'npm' -Arguments @('install') -WorkingDirectory $Parsed.PluginPath)
|
| 597 |
+
[void](Invoke-CapturedCommand -Action 'npm run build' -Command 'npm' -Arguments @('run','build') -WorkingDirectory $Parsed.PluginPath)
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
if ($Parsed.PluginPath) {
|
| 601 |
+
if ($Parsed.Copy) {
|
| 602 |
+
$arguments = @('plugins','install','--dangerously-force-unsafe-install',$Parsed.PluginPath)
|
| 603 |
+
$workingDirectory = $null
|
| 604 |
+
} else {
|
| 605 |
+
$arguments = @('plugins','install','--dangerously-force-unsafe-install','--link','.')
|
| 606 |
+
$workingDirectory = $Parsed.PluginPath
|
| 607 |
+
}
|
| 608 |
+
} else {
|
| 609 |
+
$arguments = @('plugins','install','--dangerously-force-unsafe-install',$Parsed.PluginSpec)
|
| 610 |
+
$workingDirectory = $null
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
$previous = $null
|
| 614 |
+
try {
|
| 615 |
+
if ($workingDirectory) {
|
| 616 |
+
$previous = Get-Location
|
| 617 |
+
Set-Location $workingDirectory
|
| 618 |
+
}
|
| 619 |
+
$installOutput = (& openclaw @arguments 2>&1 | Out-String).Trim()
|
| 620 |
+
$installExitCode = $LASTEXITCODE
|
| 621 |
+
} finally {
|
| 622 |
+
if ($previous) {
|
| 623 |
+
Set-Location $previous
|
| 624 |
+
}
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
if ($installExitCode -eq 0) {
|
| 628 |
+
if ($Parsed.Verbose -and $installOutput) {
|
| 629 |
+
Write-Host $installOutput
|
| 630 |
+
}
|
| 631 |
+
return
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
$lowerOutput = $installOutput.ToLowerInvariant()
|
| 635 |
+
if ($lowerOutput.Contains('plugin already exists')) {
|
| 636 |
+
Write-Host ' Plugin already installed; continuing with configuration/update steps.'
|
| 637 |
+
return
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
if ($Parsed.PluginPath -and -not $Parsed.Copy -and $lowerOutput.Contains('also not a valid hook pack')) {
|
| 641 |
+
Write-Host ' OpenClaw linked-path install bug detected; applying extension-path fallback...'
|
| 642 |
+
$targetDir = Copy-OpenClawPluginIntoExtensions -PluginPath $Parsed.PluginPath
|
| 643 |
+
Write-Host " Fallback plugin copy completed: $targetDir"
|
| 644 |
+
return
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
if (-not $installOutput) {
|
| 648 |
+
$installOutput = "exit code $installExitCode"
|
| 649 |
+
}
|
| 650 |
+
Fail "openclaw plugins install failed: $installOutput"
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
function Restart-OrStartOpenClawGateway {
|
| 654 |
+
$restartOutput = (& openclaw gateway restart 2>&1 | Out-String).Trim()
|
| 655 |
+
if ($LASTEXITCODE -eq 0) {
|
| 656 |
+
return [pscustomobject]@{ Action = 'restarted'; Output = $restartOutput }
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
$startOutput = Invoke-CapturedCommand -Action 'openclaw gateway start' -Command 'openclaw' -Arguments @('gateway','start')
|
| 660 |
+
return [pscustomobject]@{ Action = 'started'; Output = $startOutput }
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
function Invoke-OpenClawWrap {
|
| 664 |
+
param([string[]]$Arguments)
|
| 665 |
+
|
| 666 |
+
Require-Command openclaw
|
| 667 |
+
$parsed = Parse-OpenClawWrapArgs -Arguments $Arguments
|
| 668 |
+
$existingEntryJson = Get-OpenClawExistingEntryJson
|
| 669 |
+
$entryJson = Invoke-OpenClawPrepareEntryJson -ExistingEntryJson $existingEntryJson -Parsed $parsed
|
| 670 |
+
|
| 671 |
+
Write-Host ""
|
| 672 |
+
Write-Host " ╔═══════════════════════════════════════════════╗"
|
| 673 |
+
Write-Host " ║ HEADROOM WRAP: OPENCLAW ║"
|
| 674 |
+
Write-Host " ╚═══════════════════════════════════════════════╝"
|
| 675 |
+
Write-Host ""
|
| 676 |
+
if ($parsed.PluginPath) {
|
| 677 |
+
Write-Host " Plugin source: local ($($parsed.PluginPath))"
|
| 678 |
+
} else {
|
| 679 |
+
Write-Host " Plugin source: npm ($($parsed.PluginSpec))"
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
Write-Host ' Writing plugin configuration...'
|
| 683 |
+
[void](Invoke-CapturedCommand -Action 'openclaw config set plugins.entries.headroom' -Command 'openclaw' -Arguments @('config','set','plugins.entries.headroom',$entryJson,'--strict-json'))
|
| 684 |
+
Write-Host ' Installing OpenClaw plugin with required unsafe-install flag...'
|
| 685 |
+
Install-OpenClawPlugin -Parsed $parsed
|
| 686 |
+
[void](Invoke-CapturedCommand -Action 'openclaw config set plugins.slots.contextEngine' -Command 'openclaw' -Arguments @('config','set','plugins.slots.contextEngine','"headroom"','--strict-json'))
|
| 687 |
+
[void](Invoke-CapturedCommand -Action 'openclaw config validate' -Command 'openclaw' -Arguments @('config','validate'))
|
| 688 |
+
|
| 689 |
+
if ($parsed.NoRestart) {
|
| 690 |
+
Write-Host ' Skipping gateway restart (--no-restart).'
|
| 691 |
+
Write-Host ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply plugin changes.'
|
| 692 |
+
} else {
|
| 693 |
+
Write-Host ' Applying plugin changes to OpenClaw gateway...'
|
| 694 |
+
$gatewayResult = Restart-OrStartOpenClawGateway
|
| 695 |
+
Write-Host " Gateway $($gatewayResult.Action)."
|
| 696 |
+
if ($parsed.Verbose -and $gatewayResult.Output) {
|
| 697 |
+
Write-Host $gatewayResult.Output
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
$inspectOutput = Invoke-CapturedCommand -Action 'openclaw plugins inspect headroom' -Command 'openclaw' -Arguments @('plugins','inspect','headroom')
|
| 702 |
+
if ($parsed.Verbose -and $inspectOutput) {
|
| 703 |
+
Write-Host $inspectOutput
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
Write-Host ""
|
| 707 |
+
Write-Host "✓ OpenClaw is configured to use Headroom context compression."
|
| 708 |
+
Write-Host " Plugin: headroom"
|
| 709 |
+
Write-Host " Slot: plugins.slots.contextEngine = headroom"
|
| 710 |
+
Write-Host ""
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
function Invoke-OpenClawUnwrap {
|
| 714 |
+
param([string[]]$Arguments)
|
| 715 |
+
|
| 716 |
+
Require-Command openclaw
|
| 717 |
+
$parsed = Parse-OpenClawUnwrapArgs -Arguments $Arguments
|
| 718 |
+
$existingEntryJson = Get-OpenClawExistingEntryJson
|
| 719 |
+
$entryJson = Invoke-OpenClawPrepareUnwrapEntryJson -ExistingEntryJson $existingEntryJson
|
| 720 |
+
|
| 721 |
+
Write-Host ""
|
| 722 |
+
Write-Host " ╔═══════════════════════════════════════════════╗"
|
| 723 |
+
Write-Host " ║ HEADROOM UNWRAP: OPENCLAW ║"
|
| 724 |
+
Write-Host " ╚═══════════════════════════════════════════════╝"
|
| 725 |
+
Write-Host ""
|
| 726 |
+
Write-Host ' Disabling Headroom plugin and removing engine mapping...'
|
| 727 |
+
|
| 728 |
+
[void](Invoke-CapturedCommand -Action 'openclaw config set plugins.entries.headroom' -Command 'openclaw' -Arguments @('config','set','plugins.entries.headroom',$entryJson,'--strict-json'))
|
| 729 |
+
[void](Invoke-CapturedCommand -Action 'openclaw config set plugins.slots.contextEngine' -Command 'openclaw' -Arguments @('config','set','plugins.slots.contextEngine','"legacy"','--strict-json'))
|
| 730 |
+
[void](Invoke-CapturedCommand -Action 'openclaw config validate' -Command 'openclaw' -Arguments @('config','validate'))
|
| 731 |
+
|
| 732 |
+
if ($parsed.NoRestart) {
|
| 733 |
+
Write-Host ' Skipping gateway restart (--no-restart).'
|
| 734 |
+
Write-Host ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply unwrap changes.'
|
| 735 |
+
} else {
|
| 736 |
+
Write-Host ' Applying unwrap changes to OpenClaw gateway...'
|
| 737 |
+
$gatewayResult = Restart-OrStartOpenClawGateway
|
| 738 |
+
Write-Host " Gateway $($gatewayResult.Action)."
|
| 739 |
+
if ($parsed.Verbose -and $gatewayResult.Output) {
|
| 740 |
+
Write-Host $gatewayResult.Output
|
| 741 |
+
}
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
if ($parsed.Verbose) {
|
| 745 |
+
$inspectOutput = Invoke-CapturedCommand -Action 'openclaw plugins inspect headroom' -Command 'openclaw' -Arguments @('plugins','inspect','headroom')
|
| 746 |
+
if ($inspectOutput) {
|
| 747 |
+
Write-Host $inspectOutput
|
| 748 |
+
}
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
Write-Host ""
|
| 752 |
+
Write-Host "✓ OpenClaw Headroom wrap removed."
|
| 753 |
+
Write-Host " Plugin: headroom (installed, disabled)"
|
| 754 |
+
Write-Host " Slot: plugins.slots.contextEngine = legacy"
|
| 755 |
+
Write-Host ""
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
function Parse-WrapArgs {
|
| 759 |
param([string[]]$Arguments)
|
| 760 |
|
|
|
|
| 911 |
switch ($args[0]) {
|
| 912 |
'wrap' {
|
| 913 |
if ($args.Count -lt 2) {
|
| 914 |
+
Fail 'Usage: headroom wrap <claude|codex|aider|cursor|openclaw> [...]'
|
| 915 |
}
|
| 916 |
|
| 917 |
$tool = $args[1]
|
| 918 |
$wrapArgs = if ($args.Count -gt 2) { $args[2..($args.Count - 1)] } else { @() }
|
| 919 |
+
|
| 920 |
+
if ($tool -eq 'openclaw') {
|
| 921 |
+
if (Test-HelpFlag -Arguments $wrapArgs) {
|
| 922 |
+
$helpArgs = @('wrap','openclaw') + $wrapArgs
|
| 923 |
+
Invoke-HeadroomDocker -Arguments $helpArgs
|
| 924 |
+
exit 0
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
Invoke-OpenClawWrap -Arguments $wrapArgs
|
| 928 |
+
exit 0
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
$parsed = Parse-WrapArgs -Arguments $wrapArgs
|
| 932 |
$proxyArgs = New-Object System.Collections.Generic.List[string]
|
| 933 |
if ($parsed.Learn) { $proxyArgs.Add('--learn') }
|
|
|
|
| 940 |
'codex' { }
|
| 941 |
'aider' { }
|
| 942 |
'cursor' { }
|
|
|
|
| 943 |
default { Fail "Unsupported wrap target: $tool" }
|
| 944 |
}
|
| 945 |
|
|
|
|
| 991 |
}
|
| 992 |
'unwrap' {
|
| 993 |
if ($args.Count -ge 2 -and $args[1] -eq 'openclaw') {
|
| 994 |
+
$unwrapArgs = if ($args.Count -gt 2) { $args[2..($args.Count - 1)] } else { @() }
|
| 995 |
+
if (Test-HelpFlag -Arguments $unwrapArgs) {
|
| 996 |
+
$helpArgs = @('unwrap','openclaw') + $unwrapArgs
|
| 997 |
+
Invoke-HeadroomDocker -Arguments $helpArgs
|
| 998 |
+
exit 0
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
Invoke-OpenClawUnwrap -Arguments $unwrapArgs
|
| 1002 |
+
exit 0
|
| 1003 |
}
|
| 1004 |
Invoke-HeadroomDocker -Arguments $args
|
| 1005 |
}
|
scripts/install.sh
CHANGED
|
@@ -338,6 +338,451 @@ run_host_tool() {
|
|
| 338 |
"${binary}" "$@"
|
| 339 |
}
|
| 340 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
main() {
|
| 342 |
require_cmd docker
|
| 343 |
|
|
@@ -348,10 +793,19 @@ main() {
|
|
| 348 |
|
| 349 |
case "$1" in
|
| 350 |
wrap)
|
| 351 |
-
(($# >= 2)) || die "Usage: headroom wrap <claude|codex|aider|cursor> [...]"
|
| 352 |
local tool="$2"
|
| 353 |
shift 2
|
| 354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
local known_args host_args port no_rtk no_proxy learn backend anyllm region
|
| 356 |
parse_wrap_args known_args host_args port no_rtk no_proxy learn backend anyllm region "$@"
|
| 357 |
|
|
@@ -372,9 +826,6 @@ main() {
|
|
| 372 |
case "${tool}" in
|
| 373 |
claude|codex|aider|cursor)
|
| 374 |
;;
|
| 375 |
-
openclaw)
|
| 376 |
-
die "Docker-native install does not support 'headroom wrap openclaw' yet. Use a native Headroom install for OpenClaw plugin management."
|
| 377 |
-
;;
|
| 378 |
*)
|
| 379 |
die "Unsupported wrap target: ${tool}"
|
| 380 |
;;
|
|
@@ -424,7 +875,13 @@ EOF
|
|
| 424 |
;;
|
| 425 |
unwrap)
|
| 426 |
if (($# >= 2)) && [[ "$2" == "openclaw" ]]; then
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
fi
|
| 429 |
run_headroom "$@"
|
| 430 |
;;
|
|
|
|
| 338 |
"${binary}" "$@"
|
| 339 |
}
|
| 340 |
|
| 341 |
+
contains_help_flag() {
|
| 342 |
+
local arg
|
| 343 |
+
for arg in "$@"; do
|
| 344 |
+
if [[ "${arg}" == "--help" || "${arg}" == "-?" ]]; then
|
| 345 |
+
return 0
|
| 346 |
+
fi
|
| 347 |
+
done
|
| 348 |
+
|
| 349 |
+
return 1
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
parse_openclaw_wrap_args() {
|
| 353 |
+
local -n out_plugin_path=$1
|
| 354 |
+
local -n out_plugin_spec=$2
|
| 355 |
+
local -n out_skip_build=$3
|
| 356 |
+
local -n out_copy=$4
|
| 357 |
+
local -n out_proxy_port=$5
|
| 358 |
+
local -n out_startup_timeout_ms=$6
|
| 359 |
+
local -n out_gateway_provider_ids=$7
|
| 360 |
+
local -n out_python_path=$8
|
| 361 |
+
local -n out_no_auto_start=$9
|
| 362 |
+
local -n out_no_restart=${10}
|
| 363 |
+
local -n out_verbose=${11}
|
| 364 |
+
shift 11
|
| 365 |
+
|
| 366 |
+
out_plugin_path=""
|
| 367 |
+
out_plugin_spec="headroom-ai/openclaw"
|
| 368 |
+
out_skip_build=0
|
| 369 |
+
out_copy=0
|
| 370 |
+
out_proxy_port=8787
|
| 371 |
+
out_startup_timeout_ms=20000
|
| 372 |
+
out_gateway_provider_ids=()
|
| 373 |
+
out_python_path=""
|
| 374 |
+
out_no_auto_start=0
|
| 375 |
+
out_no_restart=0
|
| 376 |
+
out_verbose=0
|
| 377 |
+
|
| 378 |
+
while (($#)); do
|
| 379 |
+
case "$1" in
|
| 380 |
+
--plugin-path)
|
| 381 |
+
out_plugin_path="$2"
|
| 382 |
+
shift 2
|
| 383 |
+
;;
|
| 384 |
+
--plugin-path=*)
|
| 385 |
+
out_plugin_path="${1#*=}"
|
| 386 |
+
shift
|
| 387 |
+
;;
|
| 388 |
+
--plugin-spec)
|
| 389 |
+
out_plugin_spec="$2"
|
| 390 |
+
shift 2
|
| 391 |
+
;;
|
| 392 |
+
--plugin-spec=*)
|
| 393 |
+
out_plugin_spec="${1#*=}"
|
| 394 |
+
shift
|
| 395 |
+
;;
|
| 396 |
+
--skip-build)
|
| 397 |
+
out_skip_build=1
|
| 398 |
+
shift
|
| 399 |
+
;;
|
| 400 |
+
--copy)
|
| 401 |
+
out_copy=1
|
| 402 |
+
shift
|
| 403 |
+
;;
|
| 404 |
+
--proxy-port)
|
| 405 |
+
out_proxy_port="$2"
|
| 406 |
+
shift 2
|
| 407 |
+
;;
|
| 408 |
+
--proxy-port=*)
|
| 409 |
+
out_proxy_port="${1#*=}"
|
| 410 |
+
shift
|
| 411 |
+
;;
|
| 412 |
+
--startup-timeout-ms)
|
| 413 |
+
out_startup_timeout_ms="$2"
|
| 414 |
+
shift 2
|
| 415 |
+
;;
|
| 416 |
+
--startup-timeout-ms=*)
|
| 417 |
+
out_startup_timeout_ms="${1#*=}"
|
| 418 |
+
shift
|
| 419 |
+
;;
|
| 420 |
+
--gateway-provider-id)
|
| 421 |
+
out_gateway_provider_ids+=("$2")
|
| 422 |
+
shift 2
|
| 423 |
+
;;
|
| 424 |
+
--gateway-provider-id=*)
|
| 425 |
+
out_gateway_provider_ids+=("${1#*=}")
|
| 426 |
+
shift
|
| 427 |
+
;;
|
| 428 |
+
--python-path)
|
| 429 |
+
out_python_path="$2"
|
| 430 |
+
shift 2
|
| 431 |
+
;;
|
| 432 |
+
--python-path=*)
|
| 433 |
+
out_python_path="${1#*=}"
|
| 434 |
+
shift
|
| 435 |
+
;;
|
| 436 |
+
--no-auto-start)
|
| 437 |
+
out_no_auto_start=1
|
| 438 |
+
shift
|
| 439 |
+
;;
|
| 440 |
+
--no-restart)
|
| 441 |
+
out_no_restart=1
|
| 442 |
+
shift
|
| 443 |
+
;;
|
| 444 |
+
--verbose|-v)
|
| 445 |
+
out_verbose=1
|
| 446 |
+
shift
|
| 447 |
+
;;
|
| 448 |
+
*)
|
| 449 |
+
die "Unsupported option for 'headroom wrap openclaw': $1"
|
| 450 |
+
;;
|
| 451 |
+
esac
|
| 452 |
+
done
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
parse_openclaw_unwrap_args() {
|
| 456 |
+
local -n out_no_restart=$1
|
| 457 |
+
local -n out_verbose=$2
|
| 458 |
+
shift 2
|
| 459 |
+
|
| 460 |
+
out_no_restart=0
|
| 461 |
+
out_verbose=0
|
| 462 |
+
|
| 463 |
+
while (($#)); do
|
| 464 |
+
case "$1" in
|
| 465 |
+
--no-restart)
|
| 466 |
+
out_no_restart=1
|
| 467 |
+
shift
|
| 468 |
+
;;
|
| 469 |
+
--verbose|-v)
|
| 470 |
+
out_verbose=1
|
| 471 |
+
shift
|
| 472 |
+
;;
|
| 473 |
+
*)
|
| 474 |
+
die "Unsupported option for 'headroom unwrap openclaw': $1"
|
| 475 |
+
;;
|
| 476 |
+
esac
|
| 477 |
+
done
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
get_openclaw_existing_entry_json() {
|
| 481 |
+
local output=""
|
| 482 |
+
if output="$(openclaw config get plugins.entries.headroom 2>/dev/null)"; then
|
| 483 |
+
printf '%s' "${output}"
|
| 484 |
+
fi
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
prepare_openclaw_entry_json() {
|
| 488 |
+
local existing_entry_json="$1"
|
| 489 |
+
local proxy_port="$2"
|
| 490 |
+
local startup_timeout_ms="$3"
|
| 491 |
+
local python_path="$4"
|
| 492 |
+
local no_auto_start="$5"
|
| 493 |
+
shift 5
|
| 494 |
+
local gateway_provider_ids=("$@")
|
| 495 |
+
local args=()
|
| 496 |
+
args=(docker run --rm)
|
| 497 |
+
append_common_container_args args
|
| 498 |
+
args+=(--entrypoint headroom "${HEADROOM_IMAGE}" wrap openclaw --prepare-only)
|
| 499 |
+
args+=(--proxy-port "${proxy_port}" --startup-timeout-ms "${startup_timeout_ms}")
|
| 500 |
+
|
| 501 |
+
if [[ -n "${existing_entry_json}" ]]; then
|
| 502 |
+
args+=(--existing-entry-json "${existing_entry_json}")
|
| 503 |
+
fi
|
| 504 |
+
if [[ -n "${python_path}" ]]; then
|
| 505 |
+
args+=(--python-path "${python_path}")
|
| 506 |
+
fi
|
| 507 |
+
if [[ "${no_auto_start}" -eq 1 ]]; then
|
| 508 |
+
args+=(--no-auto-start)
|
| 509 |
+
fi
|
| 510 |
+
|
| 511 |
+
local provider_id
|
| 512 |
+
for provider_id in "${gateway_provider_ids[@]}"; do
|
| 513 |
+
args+=(--gateway-provider-id "${provider_id}")
|
| 514 |
+
done
|
| 515 |
+
|
| 516 |
+
"${args[@]}"
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
prepare_openclaw_unwrap_entry_json() {
|
| 520 |
+
local existing_entry_json="$1"
|
| 521 |
+
local args=()
|
| 522 |
+
args=(docker run --rm)
|
| 523 |
+
append_common_container_args args
|
| 524 |
+
args+=(--entrypoint headroom "${HEADROOM_IMAGE}" unwrap openclaw --prepare-only)
|
| 525 |
+
if [[ -n "${existing_entry_json}" ]]; then
|
| 526 |
+
args+=(--existing-entry-json "${existing_entry_json}")
|
| 527 |
+
fi
|
| 528 |
+
"${args[@]}"
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
run_openclaw_checked() {
|
| 532 |
+
local action="$1"
|
| 533 |
+
shift
|
| 534 |
+
local output=""
|
| 535 |
+
|
| 536 |
+
if ! output="$("$@" 2>&1)"; then
|
| 537 |
+
output="${output//$'\r'/}"
|
| 538 |
+
die "${action} failed: ${output:-unknown error}"
|
| 539 |
+
fi
|
| 540 |
+
|
| 541 |
+
printf '%s' "${output//$'\r'/}"
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
run_openclaw_checked_in_dir() {
|
| 545 |
+
local action="$1"
|
| 546 |
+
local cwd="$2"
|
| 547 |
+
shift 2
|
| 548 |
+
local output=""
|
| 549 |
+
|
| 550 |
+
if ! output="$(cd "${cwd}" && "$@" 2>&1)"; then
|
| 551 |
+
output="${output//$'\r'/}"
|
| 552 |
+
die "${action} failed: ${output:-unknown error}"
|
| 553 |
+
fi
|
| 554 |
+
|
| 555 |
+
printf '%s' "${output//$'\r'/}"
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
resolve_openclaw_extensions_dir() {
|
| 559 |
+
local config_output
|
| 560 |
+
config_output="$(run_openclaw_checked "openclaw config file" openclaw config file)"
|
| 561 |
+
local config_path
|
| 562 |
+
config_path="$(printf '%s\n' "${config_output}" | tail -n 1)"
|
| 563 |
+
[[ -n "${config_path}" ]] || die "Unable to resolve OpenClaw config path."
|
| 564 |
+
printf '%s\n' "$(dirname "${config_path}")/extensions"
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
copy_openclaw_plugin_into_extensions() {
|
| 568 |
+
local plugin_dir="$1"
|
| 569 |
+
local dist_dir="${plugin_dir}/dist"
|
| 570 |
+
local hook_shim_dir="${plugin_dir}/hook-shim"
|
| 571 |
+
[[ -d "${dist_dir}" ]] || die "Plugin dist folder missing at ${dist_dir}. Build the plugin first."
|
| 572 |
+
[[ -d "${hook_shim_dir}" ]] || die "Plugin hook-shim folder missing at ${hook_shim_dir}. Build the plugin first."
|
| 573 |
+
|
| 574 |
+
local extensions_dir
|
| 575 |
+
extensions_dir="$(resolve_openclaw_extensions_dir)"
|
| 576 |
+
local target_dir="${extensions_dir}/headroom"
|
| 577 |
+
mkdir -p "${target_dir}"
|
| 578 |
+
rm -rf "${target_dir}/dist" "${target_dir}/hook-shim"
|
| 579 |
+
cp -R "${dist_dir}" "${target_dir}/dist"
|
| 580 |
+
cp -R "${hook_shim_dir}" "${target_dir}/hook-shim"
|
| 581 |
+
|
| 582 |
+
local filename
|
| 583 |
+
for filename in openclaw.plugin.json package.json README.md; do
|
| 584 |
+
if [[ -f "${plugin_dir}/${filename}" ]]; then
|
| 585 |
+
cp "${plugin_dir}/${filename}" "${target_dir}/${filename}"
|
| 586 |
+
fi
|
| 587 |
+
done
|
| 588 |
+
|
| 589 |
+
printf '%s\n' "${target_dir}"
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
install_openclaw_plugin() {
|
| 593 |
+
local plugin_path="$1"
|
| 594 |
+
local plugin_spec="$2"
|
| 595 |
+
local skip_build="$3"
|
| 596 |
+
local copy_mode="$4"
|
| 597 |
+
local verbose="$5"
|
| 598 |
+
|
| 599 |
+
local local_source_mode=0
|
| 600 |
+
if [[ -n "${plugin_path}" ]]; then
|
| 601 |
+
local_source_mode=1
|
| 602 |
+
[[ -d "${plugin_path}" ]] || die "Plugin path not found: ${plugin_path}."
|
| 603 |
+
[[ -f "${plugin_path}/package.json" ]] || die "Invalid plugin path (missing package.json): ${plugin_path}"
|
| 604 |
+
[[ -f "${plugin_path}/openclaw.plugin.json" ]] || die "Invalid plugin path (missing openclaw.plugin.json): ${plugin_path}"
|
| 605 |
+
fi
|
| 606 |
+
|
| 607 |
+
if [[ "${local_source_mode}" -eq 1 && "${skip_build}" -eq 0 ]]; then
|
| 608 |
+
require_cmd npm
|
| 609 |
+
info "Building OpenClaw plugin (npm install + npm run build)..."
|
| 610 |
+
run_openclaw_checked_in_dir "npm install" "${plugin_path}" npm install >/dev/null
|
| 611 |
+
run_openclaw_checked_in_dir "npm run build" "${plugin_path}" npm run build >/dev/null
|
| 612 |
+
fi
|
| 613 |
+
|
| 614 |
+
local install_output=""
|
| 615 |
+
local install_status=0
|
| 616 |
+
set +e
|
| 617 |
+
if [[ "${local_source_mode}" -eq 1 ]]; then
|
| 618 |
+
if [[ "${copy_mode}" -eq 1 ]]; then
|
| 619 |
+
install_output="$(openclaw plugins install --dangerously-force-unsafe-install "${plugin_path}" 2>&1)"
|
| 620 |
+
install_status=$?
|
| 621 |
+
else
|
| 622 |
+
install_output="$(cd "${plugin_path}" && openclaw plugins install --dangerously-force-unsafe-install --link . 2>&1)"
|
| 623 |
+
install_status=$?
|
| 624 |
+
fi
|
| 625 |
+
else
|
| 626 |
+
install_output="$(openclaw plugins install --dangerously-force-unsafe-install "${plugin_spec}" 2>&1)"
|
| 627 |
+
install_status=$?
|
| 628 |
+
fi
|
| 629 |
+
set -e
|
| 630 |
+
install_output="${install_output//$'\r'/}"
|
| 631 |
+
|
| 632 |
+
if [[ "${install_status}" -eq 0 ]]; then
|
| 633 |
+
if [[ "${verbose}" -eq 1 && -n "${install_output}" ]]; then
|
| 634 |
+
printf '%s\n' "${install_output}"
|
| 635 |
+
fi
|
| 636 |
+
return
|
| 637 |
+
fi
|
| 638 |
+
|
| 639 |
+
local lower_output="${install_output,,}"
|
| 640 |
+
if [[ "${lower_output}" == *"plugin already exists"* ]]; then
|
| 641 |
+
info "Plugin already installed; continuing with configuration/update steps."
|
| 642 |
+
return
|
| 643 |
+
fi
|
| 644 |
+
|
| 645 |
+
if [[ "${lower_output}" == *"also not a valid hook pack"* && "${local_source_mode}" -eq 1 && "${copy_mode}" -eq 0 ]]; then
|
| 646 |
+
info "OpenClaw linked-path install bug detected; applying extension-path fallback..."
|
| 647 |
+
local target_dir
|
| 648 |
+
target_dir="$(copy_openclaw_plugin_into_extensions "${plugin_path}")"
|
| 649 |
+
info "Fallback plugin copy completed: ${target_dir}"
|
| 650 |
+
return
|
| 651 |
+
fi
|
| 652 |
+
|
| 653 |
+
die "openclaw plugins install failed: ${install_output:-exit code ${install_status}}"
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
restart_or_start_openclaw_gateway() {
|
| 657 |
+
local output=""
|
| 658 |
+
if output="$(openclaw gateway restart 2>&1)"; then
|
| 659 |
+
OPENCLAW_GATEWAY_ACTION="restarted"
|
| 660 |
+
OPENCLAW_GATEWAY_OUTPUT="${output//$'\r'/}"
|
| 661 |
+
return
|
| 662 |
+
fi
|
| 663 |
+
|
| 664 |
+
OPENCLAW_GATEWAY_OUTPUT="$(run_openclaw_checked "openclaw gateway start" openclaw gateway start)"
|
| 665 |
+
OPENCLAW_GATEWAY_ACTION="started"
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
wrap_openclaw_host() {
|
| 669 |
+
local plugin_path plugin_spec skip_build copy_mode proxy_port startup_timeout_ms python_path
|
| 670 |
+
local no_auto_start no_restart verbose
|
| 671 |
+
local gateway_provider_ids=()
|
| 672 |
+
|
| 673 |
+
parse_openclaw_wrap_args \
|
| 674 |
+
plugin_path \
|
| 675 |
+
plugin_spec \
|
| 676 |
+
skip_build \
|
| 677 |
+
copy_mode \
|
| 678 |
+
proxy_port \
|
| 679 |
+
startup_timeout_ms \
|
| 680 |
+
gateway_provider_ids \
|
| 681 |
+
python_path \
|
| 682 |
+
no_auto_start \
|
| 683 |
+
no_restart \
|
| 684 |
+
verbose \
|
| 685 |
+
"$@"
|
| 686 |
+
|
| 687 |
+
require_cmd openclaw
|
| 688 |
+
local existing_entry_json=""
|
| 689 |
+
existing_entry_json="$(get_openclaw_existing_entry_json)"
|
| 690 |
+
local entry_json
|
| 691 |
+
entry_json="$(prepare_openclaw_entry_json "${existing_entry_json}" "${proxy_port}" "${startup_timeout_ms}" "${python_path}" "${no_auto_start}" "${gateway_provider_ids[@]}")"
|
| 692 |
+
|
| 693 |
+
printf '\n ╔═══════════════════════════════════════════════╗\n'
|
| 694 |
+
printf ' ║ HEADROOM WRAP: OPENCLAW ║\n'
|
| 695 |
+
printf ' ╚═══════════════════════════════════════════════╝\n\n'
|
| 696 |
+
if [[ -n "${plugin_path}" ]]; then
|
| 697 |
+
printf ' Plugin source: local (%s)\n' "${plugin_path}"
|
| 698 |
+
else
|
| 699 |
+
printf ' Plugin source: npm (%s)\n' "${plugin_spec}"
|
| 700 |
+
fi
|
| 701 |
+
|
| 702 |
+
printf ' Writing plugin configuration...\n'
|
| 703 |
+
run_openclaw_checked \
|
| 704 |
+
"openclaw config set plugins.entries.headroom" \
|
| 705 |
+
openclaw config set plugins.entries.headroom "${entry_json}" --strict-json >/dev/null
|
| 706 |
+
|
| 707 |
+
printf ' Installing OpenClaw plugin with required unsafe-install flag...\n'
|
| 708 |
+
install_openclaw_plugin "${plugin_path}" "${plugin_spec}" "${skip_build}" "${copy_mode}" "${verbose}"
|
| 709 |
+
|
| 710 |
+
run_openclaw_checked \
|
| 711 |
+
"openclaw config set plugins.slots.contextEngine" \
|
| 712 |
+
openclaw config set plugins.slots.contextEngine '"headroom"' --strict-json >/dev/null
|
| 713 |
+
run_openclaw_checked "openclaw config validate" openclaw config validate >/dev/null
|
| 714 |
+
|
| 715 |
+
if [[ "${no_restart}" -eq 1 ]]; then
|
| 716 |
+
printf ' Skipping gateway restart (--no-restart).\n'
|
| 717 |
+
printf ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply plugin changes.\n'
|
| 718 |
+
else
|
| 719 |
+
printf ' Applying plugin changes to OpenClaw gateway...\n'
|
| 720 |
+
restart_or_start_openclaw_gateway
|
| 721 |
+
printf ' Gateway %s.\n' "${OPENCLAW_GATEWAY_ACTION}"
|
| 722 |
+
if [[ "${verbose}" -eq 1 && -n "${OPENCLAW_GATEWAY_OUTPUT}" ]]; then
|
| 723 |
+
printf '%s\n' "${OPENCLAW_GATEWAY_OUTPUT}"
|
| 724 |
+
fi
|
| 725 |
+
fi
|
| 726 |
+
|
| 727 |
+
local inspect_output=""
|
| 728 |
+
inspect_output="$(run_openclaw_checked "openclaw plugins inspect headroom" openclaw plugins inspect headroom)"
|
| 729 |
+
if [[ "${verbose}" -eq 1 && -n "${inspect_output}" ]]; then
|
| 730 |
+
printf '%s\n' "${inspect_output}"
|
| 731 |
+
fi
|
| 732 |
+
|
| 733 |
+
printf '\n✓ OpenClaw is configured to use Headroom context compression.\n'
|
| 734 |
+
printf ' Plugin: headroom\n'
|
| 735 |
+
printf ' Slot: plugins.slots.contextEngine = headroom\n\n'
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
unwrap_openclaw_host() {
|
| 739 |
+
local no_restart verbose
|
| 740 |
+
parse_openclaw_unwrap_args no_restart verbose "$@"
|
| 741 |
+
|
| 742 |
+
require_cmd openclaw
|
| 743 |
+
local existing_entry_json=""
|
| 744 |
+
existing_entry_json="$(get_openclaw_existing_entry_json)"
|
| 745 |
+
local entry_json
|
| 746 |
+
entry_json="$(prepare_openclaw_unwrap_entry_json "${existing_entry_json}")"
|
| 747 |
+
|
| 748 |
+
printf '\n ╔═══════════════════════════════════════════════╗\n'
|
| 749 |
+
printf ' ║ HEADROOM UNWRAP: OPENCLAW ║\n'
|
| 750 |
+
printf ' ╚═══════════════════════════════════════════════╝\n\n'
|
| 751 |
+
printf ' Disabling Headroom plugin and removing engine mapping...\n'
|
| 752 |
+
|
| 753 |
+
run_openclaw_checked \
|
| 754 |
+
"openclaw config set plugins.entries.headroom" \
|
| 755 |
+
openclaw config set plugins.entries.headroom "${entry_json}" --strict-json >/dev/null
|
| 756 |
+
run_openclaw_checked \
|
| 757 |
+
"openclaw config set plugins.slots.contextEngine" \
|
| 758 |
+
openclaw config set plugins.slots.contextEngine '"legacy"' --strict-json >/dev/null
|
| 759 |
+
run_openclaw_checked "openclaw config validate" openclaw config validate >/dev/null
|
| 760 |
+
|
| 761 |
+
if [[ "${no_restart}" -eq 1 ]]; then
|
| 762 |
+
printf ' Skipping gateway restart (--no-restart).\n'
|
| 763 |
+
printf ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply unwrap changes.\n'
|
| 764 |
+
else
|
| 765 |
+
printf ' Applying unwrap changes to OpenClaw gateway...\n'
|
| 766 |
+
restart_or_start_openclaw_gateway
|
| 767 |
+
printf ' Gateway %s.\n' "${OPENCLAW_GATEWAY_ACTION}"
|
| 768 |
+
if [[ "${verbose}" -eq 1 && -n "${OPENCLAW_GATEWAY_OUTPUT}" ]]; then
|
| 769 |
+
printf '%s\n' "${OPENCLAW_GATEWAY_OUTPUT}"
|
| 770 |
+
fi
|
| 771 |
+
fi
|
| 772 |
+
|
| 773 |
+
if [[ "${verbose}" -eq 1 ]]; then
|
| 774 |
+
local inspect_output=""
|
| 775 |
+
inspect_output="$(run_openclaw_checked "openclaw plugins inspect headroom" openclaw plugins inspect headroom)"
|
| 776 |
+
if [[ -n "${inspect_output}" ]]; then
|
| 777 |
+
printf '%s\n' "${inspect_output}"
|
| 778 |
+
fi
|
| 779 |
+
fi
|
| 780 |
+
|
| 781 |
+
printf '\n✓ OpenClaw Headroom wrap removed.\n'
|
| 782 |
+
printf ' Plugin: headroom (installed, disabled)\n'
|
| 783 |
+
printf ' Slot: plugins.slots.contextEngine = legacy\n\n'
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
main() {
|
| 787 |
require_cmd docker
|
| 788 |
|
|
|
|
| 793 |
|
| 794 |
case "$1" in
|
| 795 |
wrap)
|
| 796 |
+
(($# >= 2)) || die "Usage: headroom wrap <claude|codex|aider|cursor|openclaw> [...]"
|
| 797 |
local tool="$2"
|
| 798 |
shift 2
|
| 799 |
|
| 800 |
+
if [[ "${tool}" == "openclaw" ]]; then
|
| 801 |
+
if contains_help_flag "$@"; then
|
| 802 |
+
run_headroom wrap openclaw "$@"
|
| 803 |
+
return
|
| 804 |
+
fi
|
| 805 |
+
wrap_openclaw_host "$@"
|
| 806 |
+
return
|
| 807 |
+
fi
|
| 808 |
+
|
| 809 |
local known_args host_args port no_rtk no_proxy learn backend anyllm region
|
| 810 |
parse_wrap_args known_args host_args port no_rtk no_proxy learn backend anyllm region "$@"
|
| 811 |
|
|
|
|
| 826 |
case "${tool}" in
|
| 827 |
claude|codex|aider|cursor)
|
| 828 |
;;
|
|
|
|
|
|
|
|
|
|
| 829 |
*)
|
| 830 |
die "Unsupported wrap target: ${tool}"
|
| 831 |
;;
|
|
|
|
| 875 |
;;
|
| 876 |
unwrap)
|
| 877 |
if (($# >= 2)) && [[ "$2" == "openclaw" ]]; then
|
| 878 |
+
shift 2
|
| 879 |
+
if contains_help_flag "$@"; then
|
| 880 |
+
run_headroom unwrap openclaw "$@"
|
| 881 |
+
return
|
| 882 |
+
fi
|
| 883 |
+
unwrap_openclaw_host "$@"
|
| 884 |
+
return
|
| 885 |
fi
|
| 886 |
run_headroom "$@"
|
| 887 |
;;
|
tests/test_cli/test_wrap_bridge.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
from unittest.mock import patch
|
| 7 |
|
|
@@ -68,3 +69,56 @@ def test_wrap_cursor_prepare_only_injects_cursorrules(monkeypatch, tmp_path: Pat
|
|
| 68 |
cursorrules = Path(".cursorrules")
|
| 69 |
assert cursorrules.exists()
|
| 70 |
assert "headroom:rtk-instructions" in cursorrules.read_text()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import json
|
| 6 |
from pathlib import Path
|
| 7 |
from unittest.mock import patch
|
| 8 |
|
|
|
|
| 69 |
cursorrules = Path(".cursorrules")
|
| 70 |
assert cursorrules.exists()
|
| 71 |
assert "headroom:rtk-instructions" in cursorrules.read_text()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_wrap_openclaw_prepare_only_emits_config_without_python_default() -> None:
|
| 75 |
+
runner = CliRunner()
|
| 76 |
+
|
| 77 |
+
result = runner.invoke(
|
| 78 |
+
main,
|
| 79 |
+
[
|
| 80 |
+
"wrap",
|
| 81 |
+
"openclaw",
|
| 82 |
+
"--prepare-only",
|
| 83 |
+
"--gateway-provider-id",
|
| 84 |
+
"codex",
|
| 85 |
+
"--gateway-provider-id",
|
| 86 |
+
"anthropic",
|
| 87 |
+
],
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
assert result.exit_code == 0, result.output
|
| 91 |
+
payload = json.loads(result.output)
|
| 92 |
+
assert payload["enabled"] is True
|
| 93 |
+
assert payload["config"]["proxyPort"] == 8787
|
| 94 |
+
assert payload["config"]["gatewayProviderIds"] == ["codex", "anthropic"]
|
| 95 |
+
assert "pythonPath" not in payload["config"]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_unwrap_openclaw_prepare_only_preserves_unmanaged_config() -> None:
|
| 99 |
+
runner = CliRunner()
|
| 100 |
+
existing_entry = json.dumps(
|
| 101 |
+
{
|
| 102 |
+
"enabled": True,
|
| 103 |
+
"config": {
|
| 104 |
+
"pythonPath": "C:\\Python312\\python.exe",
|
| 105 |
+
"proxyPort": 8787,
|
| 106 |
+
"customFlag": True,
|
| 107 |
+
},
|
| 108 |
+
}
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
result = runner.invoke(
|
| 112 |
+
main,
|
| 113 |
+
[
|
| 114 |
+
"unwrap",
|
| 115 |
+
"openclaw",
|
| 116 |
+
"--prepare-only",
|
| 117 |
+
"--existing-entry-json",
|
| 118 |
+
existing_entry,
|
| 119 |
+
],
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
assert result.exit_code == 0, result.output
|
| 123 |
+
payload = json.loads(result.output)
|
| 124 |
+
assert payload == {"enabled": False, "config": {"customFlag": True}}
|
tests/test_cli/test_wrap_openclaw.py
CHANGED
|
@@ -389,6 +389,24 @@ def test_build_openclaw_plugin_entry_sets_and_clears_python_path() -> None:
|
|
| 389 |
assert without_python["config"]["customFlag"] is True
|
| 390 |
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
def test_wrap_openclaw_no_auto_start_does_not_default_python_path(
|
| 393 |
runner: CliRunner, plugin_dir: Path
|
| 394 |
) -> None:
|
|
|
|
| 389 |
assert without_python["config"]["customFlag"] is True
|
| 390 |
|
| 391 |
|
| 392 |
+
def test_build_openclaw_unwrap_entry_preserves_top_level_metadata() -> None:
|
| 393 |
+
entry = wrap_cli._build_openclaw_unwrap_entry(
|
| 394 |
+
{
|
| 395 |
+
"source": "headroom-ai/openclaw",
|
| 396 |
+
"enabled": True,
|
| 397 |
+
"config": {
|
| 398 |
+
"pythonPath": "C:\\Python312\\python.exe",
|
| 399 |
+
"proxyPort": 8787,
|
| 400 |
+
"customFlag": True,
|
| 401 |
+
},
|
| 402 |
+
}
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
assert entry["source"] == "headroom-ai/openclaw"
|
| 406 |
+
assert entry["enabled"] is False
|
| 407 |
+
assert entry["config"] == {"customFlag": True}
|
| 408 |
+
|
| 409 |
+
|
| 410 |
def test_wrap_openclaw_no_auto_start_does_not_default_python_path(
|
| 411 |
runner: CliRunner, plugin_dir: Path
|
| 412 |
) -> None:
|