feat: improve gateway services and auto-reply commands
This commit is contained in:
@@ -77,7 +77,7 @@ clawdbot gateway health --url ws://127.0.0.1:18789
|
||||
- your configured remote gateway (if set), and
|
||||
- localhost (loopback) **even if remote is configured**.
|
||||
|
||||
If multiple gateways are reachable, it prints all of them and warns this is an unconventional setup (usually you want only one gateway).
|
||||
If multiple gateways are reachable, it prints all of them. Multiple gateways are supported when you use profiles for redundancy, but most installs still run a single gateway.
|
||||
|
||||
```bash
|
||||
clawdbot gateway status
|
||||
|
||||
@@ -156,7 +156,7 @@ Chat messages support `/...` commands (text and native). See [/tools/slash-comma
|
||||
Highlights:
|
||||
- `/status` for quick diagnostics.
|
||||
- `/config` for persisted config changes.
|
||||
- `/debug` for runtime-only config overrides (memory, not disk).
|
||||
- `/debug` for runtime-only config overrides (memory, not disk; requires `commands.debug: true`).
|
||||
|
||||
## Setup + onboarding
|
||||
|
||||
@@ -448,7 +448,7 @@ Subcommands:
|
||||
Notes:
|
||||
- `daemon status` probes the Gateway RPC by default using the daemon’s resolved port/config (override with `--url/--token/--password`).
|
||||
- `daemon status` supports `--no-probe`, `--deep`, and `--json` for scripting.
|
||||
- `daemon status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans).
|
||||
- `daemon status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named Clawdbot services are treated as first-class and aren't flagged as "extra".
|
||||
- `daemon status` prints which config path the CLI uses vs which config the daemon likely uses (service env), plus the resolved probe target URL.
|
||||
- `daemon install` defaults to Node runtime; use `--runtime bun` only when WhatsApp is disabled.
|
||||
- `daemon install` options: `--port`, `--runtime`, `--token`, `--force`.
|
||||
|
||||
@@ -14,6 +14,7 @@ provider mixes reasoning into normal text.
|
||||
## Runtime debug overrides
|
||||
|
||||
Use `/debug` in chat to set **runtime-only** config overrides (memory, not disk).
|
||||
`/debug` is disabled by default; enable with `commands.debug: true`.
|
||||
This is handy when you need to toggle obscure settings without editing `clawdbot.json`.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -614,6 +614,8 @@ Controls how chat commands are enabled across connectors.
|
||||
commands: {
|
||||
native: false, // register native commands when supported
|
||||
text: true, // parse slash commands in chat messages
|
||||
config: false, // allow /config (writes to disk)
|
||||
debug: false, // allow /debug (runtime-only overrides)
|
||||
restart: false, // allow /restart + gateway restart tool
|
||||
useAccessGroups: true // enforce access-group allowlists/policies for commands
|
||||
}
|
||||
@@ -625,6 +627,8 @@ Notes:
|
||||
- `commands.text: false` disables parsing chat messages for commands.
|
||||
- `commands.native: true` registers native commands on supported connectors (Discord/Slack/Telegram). Platforms without native commands still rely on text commands.
|
||||
- `commands.native: false` skips native registration; Discord/Telegram clear previously registered commands on startup. Slack commands are managed in the Slack app.
|
||||
- `commands.config: true` enables `/config` (reads/writes `clawdbot.json`).
|
||||
- `commands.debug: true` enables `/debug` (runtime-only overrides).
|
||||
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
|
||||
- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
|
||||
|
||||
|
||||
@@ -172,8 +172,9 @@ switch to legacy names if the current image is missing.
|
||||
### 7) Gateway service migrations and cleanup hints
|
||||
Doctor detects legacy Clawdis gateway services (launchd/systemd/schtasks) and
|
||||
offers to remove them and install the Clawdbot service using the current gateway
|
||||
port. It can also scan for extra gateway-like services and print cleanup hints
|
||||
to ensure only one gateway runs per machine.
|
||||
port. It can also scan for extra gateway-like services and print cleanup hints.
|
||||
Profile-named Clawdbot gateway services are considered first-class and are not
|
||||
flagged as "extra."
|
||||
|
||||
### 8) Security warnings
|
||||
Doctor emits warnings when a provider is open to DMs without an allowlist, or
|
||||
|
||||
@@ -51,6 +51,16 @@ pnpm gateway:watch
|
||||
|
||||
Supported if you isolate state + config and use unique ports.
|
||||
|
||||
Service names are profile-aware:
|
||||
- macOS: `com.clawdbot.<profile>`
|
||||
- Linux: `clawdbot-gateway-<profile>.service`
|
||||
- Windows: `Clawdbot Gateway (<profile>)`
|
||||
|
||||
Install metadata is embedded in the service config:
|
||||
- `CLAWDBOT_SERVICE_MARKER=clawdbot`
|
||||
- `CLAWDBOT_SERVICE_KIND=gateway`
|
||||
- `CLAWDBOT_SERVICE_VERSION=<version>`
|
||||
|
||||
### Dev profile (`--dev`)
|
||||
|
||||
Fast path: run a fully-isolated dev instance (config/state/workspace) without touching your primary setup.
|
||||
@@ -160,7 +170,8 @@ See also: [Presence](/concepts/presence) for how presence is produced/deduped an
|
||||
- StandardOut/Err: file paths or `syslog`
|
||||
- On failure, launchd restarts; fatal misconfig should keep exiting so the operator notices.
|
||||
- LaunchAgents are per-user and require a logged-in session; for headless setups use a custom LaunchDaemon (not shipped).
|
||||
- `clawdbot daemon install` writes `~/Library/LaunchAgents/com.clawdbot.gateway.plist`.
|
||||
- `clawdbot daemon install` writes `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
|
||||
(or `com.clawdbot.<profile>.plist`).
|
||||
- `clawdbot doctor` audits the LaunchAgent config and can update it to current defaults.
|
||||
|
||||
## Daemon management (CLI)
|
||||
@@ -184,15 +195,18 @@ Notes:
|
||||
- `daemon status` prints config path + probe target to avoid “localhost vs LAN bind” confusion and profile mismatches.
|
||||
- `daemon status` includes the last gateway error line when the service looks running but the port is closed.
|
||||
- `logs` tails the Gateway file log via RPC (no manual `tail`/`grep` needed).
|
||||
- If other gateway-like services are detected, the CLI warns. We recommend **one gateway per machine**; one gateway can host multiple agents.
|
||||
- If other gateway-like services are detected, the CLI warns unless they are Clawdbot profile services.
|
||||
We still recommend **one gateway per machine** unless you need redundant profiles.
|
||||
- Cleanup: `clawdbot daemon uninstall` (current service) and `clawdbot doctor` (legacy migrations).
|
||||
- `daemon install` is a no-op when already installed; use `clawdbot daemon install --force` to reinstall (profile/env/path changes).
|
||||
|
||||
Bundled mac app:
|
||||
- Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled `com.clawdbot.gateway`.
|
||||
- Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled
|
||||
`com.clawdbot.gateway` (or `com.clawdbot.<profile>`).
|
||||
- To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
|
||||
- To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`).
|
||||
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first.
|
||||
- Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
||||
|
||||
## Supervision (systemd user unit)
|
||||
Clawdbot installs a **systemd user service** by default on Linux/WSL2. We
|
||||
@@ -203,10 +217,10 @@ required, shared supervision).
|
||||
`clawdbot daemon install` writes the user unit. `clawdbot doctor` audits the
|
||||
unit and can update it to match the current recommended defaults.
|
||||
|
||||
Create `~/.config/systemd/user/clawdbot-gateway.service`:
|
||||
Create `~/.config/systemd/user/clawdbot-gateway[-<profile>].service`:
|
||||
```
|
||||
[Unit]
|
||||
Description=Clawdbot Gateway
|
||||
Description=Clawdbot Gateway (profile: <profile>, v<version>)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -227,16 +241,16 @@ sudo loginctl enable-linger youruser
|
||||
Onboarding runs this on Linux/WSL2 (may prompt for sudo; writes `/var/lib/systemd/linger`).
|
||||
Then enable the service:
|
||||
```
|
||||
systemctl --user enable --now clawdbot-gateway.service
|
||||
systemctl --user enable --now clawdbot-gateway[-<profile>].service
|
||||
```
|
||||
|
||||
**Alternative (system service)** - for always-on or multi-user servers, you can
|
||||
install a systemd **system** unit instead of a user unit (no lingering needed).
|
||||
Create `/etc/systemd/system/clawdbot-gateway.service` (copy the unit above,
|
||||
Create `/etc/systemd/system/clawdbot-gateway[-<profile>].service` (copy the unit above,
|
||||
switch `WantedBy=multi-user.target`, set `User=` + `WorkingDirectory=`), then:
|
||||
```
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now clawdbot-gateway.service
|
||||
sudo systemctl enable --now clawdbot-gateway[-<profile>].service
|
||||
```
|
||||
|
||||
## Windows (WSL2)
|
||||
@@ -249,7 +263,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above.
|
||||
- Debug: subscribe to `tick` and `presence` events; ensure `status` shows linked/auth age; presence entries show Gateway host and connected clients.
|
||||
|
||||
## Safety guarantees
|
||||
- Only one Gateway per host; all sends/agent calls must go through it.
|
||||
- Assume one Gateway per host by default; if you run multiple profiles, isolate ports/state and target the right instance.
|
||||
- No fallback to direct Baileys connections; if the Gateway is down, sends fail fast.
|
||||
- Non-connect first frames or malformed JSON are rejected and the socket is closed.
|
||||
- Graceful shutdown: emit `shutdown` event before closing; clients must handle close + reconnect.
|
||||
|
||||
@@ -48,8 +48,8 @@ Doctor/daemon will show runtime state (PID/last exit) and log hints.
|
||||
- Preferred: `clawdbot logs --follow`
|
||||
- File logs (always): `/tmp/clawdbot/clawdbot-YYYY-MM-DD.log` (or your configured `logging.file`)
|
||||
- macOS LaunchAgent (if installed): `$CLAWDBOT_STATE_DIR/logs/gateway.log` and `gateway.err.log`
|
||||
- Linux systemd (if installed): `journalctl --user -u clawdbot-gateway.service -n 200 --no-pager`
|
||||
- Windows: `schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST`
|
||||
- Linux systemd (if installed): `journalctl --user -u clawdbot-gateway[-<profile>].service -n 200 --no-pager`
|
||||
- Windows: `schtasks /Query /TN "Clawdbot Gateway (<profile>)" /V /FO LIST`
|
||||
|
||||
**Enable more logging:**
|
||||
- Bump file log detail (persisted JSONL):
|
||||
@@ -324,7 +324,7 @@ If the gateway is supervised by launchd, killing the PID will just respawn it. S
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot daemon stop
|
||||
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway
|
||||
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway (replace with com.clawdbot.<profile> if needed)
|
||||
```
|
||||
|
||||
**Fix 2: Port is busy (find the listener)**
|
||||
@@ -360,7 +360,7 @@ clawdbot providers login --verbose
|
||||
| Log | Location |
|
||||
|-----|----------|
|
||||
| Gateway file logs (structured) | `/tmp/clawdbot/clawdbot-YYYY-MM-DD.log` (or `logging.file`) |
|
||||
| Gateway service logs (supervisor) | macOS: `$CLAWDBOT_STATE_DIR/logs/gateway.log` + `gateway.err.log` (default: `~/.clawdbot/logs/...`; profiles use `~/.clawdbot-<profile>/logs/...`)<br />Linux: `journalctl --user -u clawdbot-gateway.service -n 200 --no-pager`<br />Windows: `schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST` |
|
||||
| Gateway service logs (supervisor) | macOS: `$CLAWDBOT_STATE_DIR/logs/gateway.log` + `gateway.err.log` (default: `~/.clawdbot/logs/...`; profiles use `~/.clawdbot-<profile>/logs/...`)<br />Linux: `journalctl --user -u clawdbot-gateway[-<profile>].service -n 200 --no-pager`<br />Windows: `schtasks /Query /TN "Clawdbot Gateway (<profile>)" /V /FO LIST` |
|
||||
| Session files | `$CLAWDBOT_STATE_DIR/agents/<agentId>/sessions/` |
|
||||
| Media cache | `$CLAWDBOT_STATE_DIR/media/` |
|
||||
| Credentials | `$CLAWDBOT_STATE_DIR/credentials/` |
|
||||
|
||||
@@ -124,9 +124,9 @@ clawdbot logs --follow
|
||||
```
|
||||
|
||||
If you’re supervised:
|
||||
- macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`
|
||||
- Linux systemd user service: `systemctl --user restart clawdbot-gateway.service`
|
||||
- Windows (WSL2): `systemctl --user restart clawdbot-gateway.service`
|
||||
- macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/com.clawdbot.gateway` (use `com.clawdbot.<profile>` if set)
|
||||
- Linux systemd user service: `systemctl --user restart clawdbot-gateway[-<profile>].service`
|
||||
- Windows (WSL2): `systemctl --user restart clawdbot-gateway[-<profile>].service`
|
||||
- `launchctl`/`systemctl` only work if the service is installed; otherwise run `clawdbot daemon install`.
|
||||
|
||||
Runbook + exact service labels: [Gateway runbook](/gateway)
|
||||
|
||||
@@ -164,7 +164,7 @@ Control UI details: [Control UI](/web/control-ui)
|
||||
On Linux, Clawdbot uses a systemd **user** service. After `--install-daemon`, verify:
|
||||
|
||||
```bash
|
||||
systemctl --user status clawdbot-gateway.service
|
||||
systemctl --user status clawdbot-gateway[-<profile>].service
|
||||
```
|
||||
|
||||
If the service dies after logout, enable lingering:
|
||||
|
||||
@@ -42,5 +42,5 @@ Use one of these (all supported):
|
||||
- Repair/migrate: `clawdbot doctor` (offers to install or fix the service)
|
||||
|
||||
The service target depends on OS:
|
||||
- macOS: LaunchAgent (`com.clawdbot.gateway`)
|
||||
- Linux/WSL2: systemd user service
|
||||
- macOS: LaunchAgent (`com.clawdbot.gateway` or `com.clawdbot.<profile>`)
|
||||
- Linux/WSL2: systemd user service (`clawdbot-gateway[-<profile>].service`)
|
||||
|
||||
@@ -64,11 +64,11 @@ live in the [Gateway runbook](/gateway).
|
||||
|
||||
Minimal setup:
|
||||
|
||||
Create `~/.config/systemd/user/clawdbot-gateway.service`:
|
||||
Create `~/.config/systemd/user/clawdbot-gateway[-<profile>].service`:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Clawdbot Gateway
|
||||
Description=Clawdbot Gateway (profile: <profile>, v<version>)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -84,5 +84,5 @@ WantedBy=default.target
|
||||
Enable it:
|
||||
|
||||
```
|
||||
systemctl --user enable --now clawdbot-gateway.service
|
||||
systemctl --user enable --now clawdbot-gateway[-<profile>].service
|
||||
```
|
||||
|
||||
@@ -63,10 +63,10 @@ Version injection:
|
||||
## Launchd (Gateway as LaunchAgent)
|
||||
|
||||
Label:
|
||||
- `com.clawdbot.gateway`
|
||||
- `com.clawdbot.gateway` (or `com.clawdbot.<profile>`)
|
||||
|
||||
Plist location (per-user):
|
||||
- `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
|
||||
- `~/Library/LaunchAgents/com.clawdbot.gateway.plist` (or `.../com.clawdbot.<profile>.plist`)
|
||||
|
||||
Manager:
|
||||
- The macOS app owns LaunchAgent install/update for the bundled gateway.
|
||||
|
||||
@@ -14,7 +14,8 @@ manually in a terminal.
|
||||
|
||||
## Default behavior (launchd)
|
||||
|
||||
- The app installs a per‑user LaunchAgent labeled `com.clawdbot.gateway`.
|
||||
- The app installs a per‑user LaunchAgent labeled `com.clawdbot.gateway`
|
||||
(or `com.clawdbot.<profile>` when using `--profile`/`CLAWDBOT_PROFILE`).
|
||||
- When Local mode is enabled, the app ensures the LaunchAgent is loaded and
|
||||
starts the Gateway if needed.
|
||||
- Logs are written to the launchd gateway log path (visible in Debug Settings).
|
||||
@@ -26,6 +27,8 @@ launchctl kickstart -k gui/$UID/com.clawdbot.gateway
|
||||
launchctl bootout gui/$UID/com.clawdbot.gateway
|
||||
```
|
||||
|
||||
Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
||||
|
||||
## Attach‑only (developer mode)
|
||||
|
||||
Attach‑only tells the app to **connect to an existing Gateway** without spawning
|
||||
|
||||
@@ -31,13 +31,16 @@ node.
|
||||
|
||||
## Launchd control
|
||||
|
||||
The app manages a per‑user LaunchAgent labeled `com.clawdbot.gateway`.
|
||||
The app manages a per‑user LaunchAgent labeled `com.clawdbot.gateway`
|
||||
(or `com.clawdbot.<profile>` when using `--profile`/`CLAWDBOT_PROFILE`).
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$UID/com.clawdbot.gateway
|
||||
launchctl bootout gui/$UID/com.clawdbot.gateway
|
||||
```
|
||||
|
||||
Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
||||
|
||||
If the LaunchAgent isn’t installed, enable it from the app or run
|
||||
`clawdbot daemon install`.
|
||||
|
||||
|
||||
@@ -606,6 +606,8 @@ Yes, but you must isolate:
|
||||
- `gateway.port` (unique ports)
|
||||
|
||||
There are convenience CLI flags like `--dev` and `--profile <name>` that shift state dirs and ports.
|
||||
When using profiles, service names are suffixed (`com.clawdbot.<profile>`, `clawdbot-gateway-<profile>.service`,
|
||||
`Clawdbot Gateway (<profile>)`).
|
||||
|
||||
## Logging and debugging
|
||||
|
||||
@@ -627,8 +629,8 @@ clawdbot logs --follow
|
||||
|
||||
Service/supervisor logs (when the gateway runs via launchd/systemd):
|
||||
- macOS: `$CLAWDBOT_STATE_DIR/logs/gateway.log` and `gateway.err.log` (default: `~/.clawdbot/logs/...`; profiles use `~/.clawdbot-<profile>/logs/...`)
|
||||
- Linux: `journalctl --user -u clawdbot-gateway.service -n 200 --no-pager`
|
||||
- Windows: `schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST`
|
||||
- Linux: `journalctl --user -u clawdbot-gateway[-<profile>].service -n 200 --no-pager`
|
||||
- Windows: `schtasks /Query /TN "Clawdbot Gateway (<profile>)" /V /FO LIST`
|
||||
|
||||
See [Troubleshooting](/gateway/troubleshooting#log-locations) for more.
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ Directives (`/think`, `/verbose`, `/reasoning`, `/elevated`) are parsed even whe
|
||||
commands: {
|
||||
native: false,
|
||||
text: true,
|
||||
config: false,
|
||||
debug: false,
|
||||
restart: false,
|
||||
useAccessGroups: true
|
||||
}
|
||||
@@ -29,6 +31,8 @@ Directives (`/think`, `/verbose`, `/reasoning`, `/elevated`) are parsed even whe
|
||||
- `commands.native` (default `false`) registers native commands on Discord/Slack/Telegram.
|
||||
- `false` clears previously registered commands on Discord/Telegram at startup.
|
||||
- Slack commands are managed in the Slack app and are not removed automatically.
|
||||
- `commands.config` (default `false`) enables `/config` (reads/writes `clawdbot.json`).
|
||||
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
|
||||
- `commands.useAccessGroups` (default `true`) enforces allowlists/policies for commands.
|
||||
|
||||
## Command list
|
||||
@@ -39,8 +43,8 @@ Text + native (when enabled):
|
||||
- `/status`
|
||||
- `/status` (show current status; includes a short usage line when available)
|
||||
- `/usage` (alias: `/status`)
|
||||
- `/config show|get|set|unset` (persist config to disk, owner-only)
|
||||
- `/debug show|set|unset|reset` (runtime overrides, owner-only)
|
||||
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
|
||||
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
|
||||
- `/cost on|off` (toggle per-response usage line)
|
||||
- `/stop`
|
||||
- `/restart`
|
||||
@@ -67,7 +71,7 @@ Notes:
|
||||
|
||||
## Debug overrides
|
||||
|
||||
`/debug` lets you set **runtime-only** config overrides (memory, not disk). Owner-only.
|
||||
`/debug` lets you set **runtime-only** config overrides (memory, not disk). Owner-only. Disabled by default; enable with `commands.debug: true`.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -85,7 +89,7 @@ Notes:
|
||||
|
||||
## Config updates
|
||||
|
||||
`/config` writes to your on-disk config (`clawdbot.json`). Owner-only.
|
||||
`/config` writes to your on-disk config (`clawdbot.json`). Owner-only. Disabled by default; enable with `commands.config: true`.
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ describe("control command parsing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("respects disabled config/debug commands", () => {
|
||||
const cfg = { commands: { config: false, debug: false } };
|
||||
expect(hasControlCommand("/config show", cfg)).toBe(false);
|
||||
expect(hasControlCommand("/debug show", cfg)).toBe(false);
|
||||
});
|
||||
|
||||
it("requires commands to be the full message", () => {
|
||||
expect(hasControlCommand("hello /status")).toBe(false);
|
||||
expect(hasControlCommand("/status please")).toBe(false);
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { listChatCommands, normalizeCommandBody } from "./commands-registry.js";
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import {
|
||||
listChatCommands,
|
||||
listChatCommandsForConfig,
|
||||
normalizeCommandBody,
|
||||
} from "./commands-registry.js";
|
||||
|
||||
export function hasControlCommand(text?: string): boolean {
|
||||
export function hasControlCommand(
|
||||
text?: string,
|
||||
cfg?: ClawdbotConfig,
|
||||
): boolean {
|
||||
if (!text) return false;
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return false;
|
||||
const normalizedBody = normalizeCommandBody(trimmed);
|
||||
if (!normalizedBody) return false;
|
||||
const lowered = normalizedBody.toLowerCase();
|
||||
for (const command of listChatCommands()) {
|
||||
const commands = cfg ? listChatCommandsForConfig(cfg) : listChatCommands();
|
||||
for (const command of commands) {
|
||||
for (const alias of command.textAliases) {
|
||||
const normalized = alias.trim().toLowerCase();
|
||||
if (!normalized) continue;
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
buildCommandText,
|
||||
getCommandDetection,
|
||||
listChatCommands,
|
||||
listChatCommandsForConfig,
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
shouldHandleTextCommands,
|
||||
} from "./commands-registry.js";
|
||||
|
||||
@@ -21,6 +23,26 @@ describe("commands registry", () => {
|
||||
expect(specs.find((spec) => spec.name === "compact")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("filters commands based on config flags", () => {
|
||||
const disabled = listChatCommandsForConfig({
|
||||
commands: { config: false, debug: false },
|
||||
});
|
||||
expect(disabled.find((spec) => spec.key === "config")).toBeFalsy();
|
||||
expect(disabled.find((spec) => spec.key === "debug")).toBeFalsy();
|
||||
|
||||
const enabled = listChatCommandsForConfig({
|
||||
commands: { config: true, debug: true },
|
||||
});
|
||||
expect(enabled.find((spec) => spec.key === "config")).toBeTruthy();
|
||||
expect(enabled.find((spec) => spec.key === "debug")).toBeTruthy();
|
||||
|
||||
const nativeDisabled = listNativeCommandSpecsForConfig({
|
||||
commands: { config: false, debug: false, native: true },
|
||||
});
|
||||
expect(nativeDisabled.find((spec) => spec.name === "config")).toBeFalsy();
|
||||
expect(nativeDisabled.find((spec) => spec.name === "debug")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects known text commands", () => {
|
||||
const detection = getCommandDetection();
|
||||
expect(detection.exact.has("/commands")).toBe(true);
|
||||
|
||||
@@ -290,6 +290,21 @@ export function listChatCommands(): ChatCommandDefinition[] {
|
||||
return [...CHAT_COMMANDS];
|
||||
}
|
||||
|
||||
export function isCommandEnabled(
|
||||
cfg: ClawdbotConfig,
|
||||
commandKey: string,
|
||||
): boolean {
|
||||
if (commandKey === "config") return cfg.commands?.config === true;
|
||||
if (commandKey === "debug") return cfg.commands?.debug === true;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function listChatCommandsForConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
): ChatCommandDefinition[] {
|
||||
return CHAT_COMMANDS.filter((command) => isCommandEnabled(cfg, command.key));
|
||||
}
|
||||
|
||||
export function listNativeCommandSpecs(): NativeCommandSpec[] {
|
||||
return CHAT_COMMANDS.filter(
|
||||
(command) => command.scope !== "text" && command.nativeName,
|
||||
@@ -300,6 +315,18 @@ export function listNativeCommandSpecs(): NativeCommandSpec[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function listNativeCommandSpecsForConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
): NativeCommandSpec[] {
|
||||
return listChatCommandsForConfig(cfg)
|
||||
.filter((command) => command.scope !== "text" && command.nativeName)
|
||||
.map((command) => ({
|
||||
name: command.nativeName ?? command.key,
|
||||
description: command.description,
|
||||
acceptsArgs: Boolean(command.acceptsArgs),
|
||||
}));
|
||||
}
|
||||
|
||||
export function findCommandByNativeName(
|
||||
name: string,
|
||||
): ChatCommandDefinition | undefined {
|
||||
|
||||
@@ -877,7 +877,7 @@ export async function getReplyFromConfig(
|
||||
allowTextCommands &&
|
||||
!commandAuthorized &&
|
||||
!baseBodyTrimmedRaw &&
|
||||
hasControlCommand(commandSource)
|
||||
hasControlCommand(commandSource, cfg)
|
||||
) {
|
||||
typing.cleanup();
|
||||
return undefined;
|
||||
|
||||
@@ -602,7 +602,7 @@ export async function handleCommands(params: {
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
return { shouldContinue: false, reply: { text: buildHelpMessage() } };
|
||||
return { shouldContinue: false, reply: { text: buildHelpMessage(cfg) } };
|
||||
}
|
||||
|
||||
const commandsRequested = command.commandBodyNormalized === "/commands";
|
||||
@@ -613,7 +613,7 @@ export async function handleCommands(params: {
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
return { shouldContinue: false, reply: { text: buildCommandsMessage() } };
|
||||
return { shouldContinue: false, reply: { text: buildCommandsMessage(cfg) } };
|
||||
}
|
||||
|
||||
const statusRequested =
|
||||
@@ -650,6 +650,14 @@ export async function handleCommands(params: {
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
if (cfg.commands?.config !== true) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚠️ /config is disabled. Set commands.config=true to enable.",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (configCommand.action === "error") {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
@@ -774,6 +782,14 @@ export async function handleCommands(params: {
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
if (cfg.commands?.debug !== true) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚠️ /debug is disabled. Set commands.debug=true to enable.",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (debugCommand.action === "error") {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
|
||||
@@ -4,7 +4,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { buildCommandsMessage, buildStatusMessage } from "./status.js";
|
||||
import {
|
||||
buildCommandsMessage,
|
||||
buildHelpMessage,
|
||||
buildStatusMessage,
|
||||
} from "./status.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -317,7 +321,9 @@ describe("buildStatusMessage", () => {
|
||||
|
||||
describe("buildCommandsMessage", () => {
|
||||
it("lists commands with aliases and text-only hints", () => {
|
||||
const text = buildCommandsMessage();
|
||||
const text = buildCommandsMessage({
|
||||
commands: { config: false, debug: false },
|
||||
} as ClawdbotConfig);
|
||||
expect(text).toContain("/commands - List all slash commands.");
|
||||
expect(text).toContain(
|
||||
"/think (aliases: /thinking, /t) - Set thinking level.",
|
||||
@@ -325,5 +331,17 @@ describe("buildCommandsMessage", () => {
|
||||
expect(text).toContain(
|
||||
"/compact (text-only) - Compact the session context.",
|
||||
);
|
||||
expect(text).not.toContain("/config");
|
||||
expect(text).not.toContain("/debug");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildHelpMessage", () => {
|
||||
it("hides config/debug when disabled", () => {
|
||||
const text = buildHelpMessage({
|
||||
commands: { config: false, debug: false },
|
||||
} as ClawdbotConfig);
|
||||
expect(text).not.toContain("/config");
|
||||
expect(text).not.toContain("/debug");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,10 @@ import {
|
||||
resolveModelCostConfig,
|
||||
} from "../utils/usage-format.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { listChatCommands } from "./commands-registry.js";
|
||||
import {
|
||||
listChatCommands,
|
||||
listChatCommandsForConfig,
|
||||
} from "./commands-registry.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
@@ -356,18 +359,29 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function buildHelpMessage(): string {
|
||||
export function buildHelpMessage(cfg?: ClawdbotConfig): string {
|
||||
const options = [
|
||||
"/think <level>",
|
||||
"/verbose on|off",
|
||||
"/reasoning on|off",
|
||||
"/elevated on|off",
|
||||
"/model <id>",
|
||||
"/cost on|off",
|
||||
];
|
||||
if (cfg?.commands?.config === true) options.push("/config show");
|
||||
if (cfg?.commands?.debug === true) options.push("/debug show");
|
||||
return [
|
||||
"ℹ️ Help",
|
||||
"Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
|
||||
"Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id> | /cost on|off | /config show | /debug show",
|
||||
`Options: ${options.join(" | ")}`,
|
||||
"More: /commands for all slash commands",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildCommandsMessage(): string {
|
||||
export function buildCommandsMessage(cfg?: ClawdbotConfig): string {
|
||||
const lines = ["ℹ️ Slash commands"];
|
||||
for (const command of listChatCommands()) {
|
||||
const commands = cfg ? listChatCommandsForConfig(cfg) : listChatCommands();
|
||||
for (const command of commands) {
|
||||
const primary = command.nativeName
|
||||
? `/${command.nativeName}`
|
||||
: command.textAliases[0]?.trim() || `/${command.key}`;
|
||||
|
||||
@@ -19,9 +19,9 @@ import type {
|
||||
GatewayControlUiConfig,
|
||||
} from "../config/types.js";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "../daemon/constants.js";
|
||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||
import {
|
||||
@@ -309,31 +309,42 @@ function renderRuntimeHints(
|
||||
hints.push(`Launchd stdout (if installed): ${logs.stdoutPath}`);
|
||||
hints.push(`Launchd stderr (if installed): ${logs.stderrPath}`);
|
||||
} else if (process.platform === "linux") {
|
||||
const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(
|
||||
"Logs: journalctl --user -u clawdbot-gateway.service -n 200 --no-pager",
|
||||
`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`,
|
||||
);
|
||||
} else if (process.platform === "win32") {
|
||||
hints.push('Logs: schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST');
|
||||
const task = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(`Logs: schtasks /Query /TN "${task}" /V /FO LIST`);
|
||||
}
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
|
||||
function renderGatewayServiceStartHints(): string[] {
|
||||
function renderGatewayServiceStartHints(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const base = ["clawdbot daemon install", "clawdbot gateway"];
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
case "darwin": {
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
return [
|
||||
...base,
|
||||
`launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`,
|
||||
`launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${label}.plist`,
|
||||
];
|
||||
case "linux":
|
||||
}
|
||||
case "linux": {
|
||||
const unit = resolveGatewaySystemdServiceName(profile);
|
||||
return [
|
||||
...base,
|
||||
`systemctl --user start ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||
`systemctl --user start ${unit}.service`,
|
||||
];
|
||||
case "win32":
|
||||
return [...base, `schtasks /Run /TN "${GATEWAY_WINDOWS_TASK_NAME}"`];
|
||||
}
|
||||
case "win32": {
|
||||
const task = resolveGatewayWindowsTaskName(profile);
|
||||
return [...base, `schtasks /Run /TN "${task}"`];
|
||||
}
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
@@ -346,7 +357,9 @@ async function gatherDaemonStatus(opts: {
|
||||
}): Promise<DaemonStatus> {
|
||||
const service = resolveGatewayService();
|
||||
const [loaded, command, runtime] = await Promise.all([
|
||||
service.isLoaded({ env: process.env }).catch(() => false),
|
||||
service
|
||||
.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })
|
||||
.catch(() => false),
|
||||
service.readCommand(process.env).catch(() => null),
|
||||
service.readRuntime(process.env).catch(() => undefined),
|
||||
]);
|
||||
@@ -713,9 +726,11 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
||||
spacer();
|
||||
}
|
||||
if (service.runtime?.cachedLabel) {
|
||||
const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv;
|
||||
const label = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
defaultRuntime.error(
|
||||
errorText(
|
||||
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${label}`,
|
||||
),
|
||||
);
|
||||
defaultRuntime.error(errorText("Then reinstall: clawdbot daemon install"));
|
||||
@@ -767,9 +782,11 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
||||
);
|
||||
}
|
||||
if (process.platform === "linux") {
|
||||
const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv;
|
||||
const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
defaultRuntime.error(
|
||||
errorText(
|
||||
`Logs: journalctl --user -u ${GATEWAY_SYSTEMD_SERVICE_NAME}.service -n 200 --no-pager`,
|
||||
`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`,
|
||||
),
|
||||
);
|
||||
} else if (process.platform === "darwin") {
|
||||
@@ -872,9 +889,10 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
}
|
||||
|
||||
const service = resolveGatewayService();
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -910,7 +928,9 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
cfg.gateway?.auth?.token ||
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
launchdLabel:
|
||||
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(profile)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -945,9 +965,10 @@ export async function runDaemonUninstall() {
|
||||
|
||||
export async function runDaemonStart() {
|
||||
const service = resolveGatewayService();
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -961,7 +982,7 @@ export async function runDaemonStart() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
await service.restart({ profile, stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway start failed: ${String(err)}`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
@@ -973,9 +994,10 @@ export async function runDaemonStart() {
|
||||
|
||||
export async function runDaemonStop() {
|
||||
const service = resolveGatewayService();
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -986,7 +1008,7 @@ export async function runDaemonStop() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.stop({ stdout: process.stdout });
|
||||
await service.stop({ profile, stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -1000,9 +1022,10 @@ export async function runDaemonStop() {
|
||||
*/
|
||||
export async function runDaemonRestart(): Promise<boolean> {
|
||||
const service = resolveGatewayService();
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -1016,7 +1039,7 @@ export async function runDaemonRestart(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
await service.restart({ profile, stdout: process.stdout });
|
||||
return true;
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "../daemon/constants.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
@@ -362,22 +362,25 @@ function extractGatewayMiskeys(parsed: unknown): {
|
||||
return { hasGatewayToken, hasRemoteToken };
|
||||
}
|
||||
|
||||
function renderGatewayServiceStopHints(): string[] {
|
||||
function renderGatewayServiceStopHints(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return [
|
||||
"Tip: clawdbot daemon stop",
|
||||
`Or: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
`Or: launchctl bootout gui/$UID/${resolveGatewayLaunchAgentLabel(profile)}`,
|
||||
];
|
||||
case "linux":
|
||||
return [
|
||||
"Tip: clawdbot daemon stop",
|
||||
`Or: systemctl --user stop ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||
`Or: systemctl --user stop ${resolveGatewaySystemdServiceName(profile)}.service`,
|
||||
];
|
||||
case "win32":
|
||||
return [
|
||||
"Tip: clawdbot daemon stop",
|
||||
`Or: schtasks /End /TN "${GATEWAY_WINDOWS_TASK_NAME}"`,
|
||||
`Or: schtasks /End /TN "${resolveGatewayWindowsTaskName(profile)}"`,
|
||||
];
|
||||
default:
|
||||
return ["Tip: clawdbot daemon stop"];
|
||||
@@ -388,7 +391,7 @@ async function maybeExplainGatewayServiceStop() {
|
||||
const service = resolveGatewayService();
|
||||
let loaded: boolean | null = null;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
|
||||
} catch {
|
||||
loaded = null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
resolveGatewayPort,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
@@ -340,7 +340,9 @@ async function maybeInstallDaemon(params: {
|
||||
daemonRuntime?: GatewayDaemonRuntime;
|
||||
}) {
|
||||
const service = resolveGatewayService();
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
const loaded = await service.isLoaded({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
});
|
||||
let shouldCheckLinger = false;
|
||||
let shouldInstall = true;
|
||||
let daemonRuntime = params.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
@@ -357,7 +359,10 @@ async function maybeInstallDaemon(params: {
|
||||
params.runtime,
|
||||
);
|
||||
if (action === "restart") {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
await service.restart({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
stdout: process.stdout,
|
||||
});
|
||||
shouldCheckLinger = true;
|
||||
shouldInstall = false;
|
||||
}
|
||||
@@ -397,7 +402,9 @@ async function maybeInstallDaemon(params: {
|
||||
port: params.port,
|
||||
token: params.gatewayToken,
|
||||
launchdLabel:
|
||||
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
|
||||
: undefined,
|
||||
});
|
||||
await service.install({
|
||||
env: process.env,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import {
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "../daemon/constants.js";
|
||||
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
|
||||
import type { GatewayServiceRuntime } from "../daemon/service-runtime.js";
|
||||
import { getResolvedLoggerSettings } from "../logging.js";
|
||||
@@ -51,8 +55,9 @@ export function buildGatewayRuntimeHints(
|
||||
}
|
||||
})();
|
||||
if (runtime.cachedLabel && platform === "darwin") {
|
||||
const label = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
hints.push(
|
||||
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${label}`,
|
||||
);
|
||||
hints.push("Then reinstall: clawdbot daemon install");
|
||||
}
|
||||
@@ -71,11 +76,13 @@ export function buildGatewayRuntimeHints(
|
||||
hints.push(`Launchd stdout (if installed): ${logs.stdoutPath}`);
|
||||
hints.push(`Launchd stderr (if installed): ${logs.stderrPath}`);
|
||||
} else if (platform === "linux") {
|
||||
const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(
|
||||
"Logs: journalctl --user -u clawdbot-gateway.service -n 200 --no-pager",
|
||||
`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`,
|
||||
);
|
||||
} else if (platform === "win32") {
|
||||
hints.push('Logs: schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST');
|
||||
const task = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(`Logs: schtasks /Query /TN "${task}" /V /FO LIST`);
|
||||
}
|
||||
}
|
||||
return hints;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { note as clackNote } from "@clack/prompts";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import {
|
||||
findExtraGatewayServices,
|
||||
renderGatewayServiceCleanupHints,
|
||||
@@ -103,7 +103,9 @@ export async function maybeMigrateLegacyGatewayService(
|
||||
}
|
||||
|
||||
const service = resolveGatewayService();
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
const loaded = await service.isLoaded({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
});
|
||||
if (loaded) {
|
||||
note(`Clawdbot ${service.label} already ${service.loadedText}.`, "Gateway");
|
||||
return;
|
||||
@@ -143,7 +145,9 @@ export async function maybeMigrateLegacyGatewayService(
|
||||
port,
|
||||
token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
launchdLabel:
|
||||
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
|
||||
: undefined,
|
||||
});
|
||||
await service.install({
|
||||
env: process.env,
|
||||
@@ -263,7 +267,9 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
port,
|
||||
token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
launchdLabel:
|
||||
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
resolveGatewayPort,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
@@ -421,7 +421,9 @@ export async function doctorCommand(
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
loaded = await service.isLoaded({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
});
|
||||
} catch {
|
||||
loaded = false;
|
||||
}
|
||||
@@ -503,7 +505,9 @@ export async function doctorCommand(
|
||||
|
||||
if (!healthOk) {
|
||||
const service = resolveGatewayService();
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
const loaded = await service.isLoaded({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
});
|
||||
let serviceRuntime:
|
||||
| Awaited<ReturnType<typeof service.readRuntime>>
|
||||
| undefined;
|
||||
@@ -562,7 +566,7 @@ export async function doctorCommand(
|
||||
cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
launchdLabel:
|
||||
process.platform === "darwin"
|
||||
? GATEWAY_LAUNCH_AGENT_LABEL
|
||||
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
|
||||
: undefined,
|
||||
});
|
||||
await service.install({
|
||||
@@ -592,13 +596,19 @@ export async function doctorCommand(
|
||||
initialValue: true,
|
||||
});
|
||||
if (start) {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
await service.restart({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
stdout: process.stdout,
|
||||
});
|
||||
await sleep(1500);
|
||||
}
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
const label = resolveGatewayLaunchAgentLabel(
|
||||
process.env.CLAWDBOT_PROFILE,
|
||||
);
|
||||
note(
|
||||
`LaunchAgent loaded; stopping requires "clawdbot daemon stop" or launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}.`,
|
||||
`LaunchAgent loaded; stopping requires "clawdbot daemon stop" or launchctl bootout gui/$UID/${label}.`,
|
||||
"Gateway",
|
||||
);
|
||||
}
|
||||
@@ -608,7 +618,10 @@ export async function doctorCommand(
|
||||
initialValue: true,
|
||||
});
|
||||
if (restart) {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
await service.restart({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
stdout: process.stdout,
|
||||
});
|
||||
await sleep(1500);
|
||||
try {
|
||||
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
resolveGatewayPort,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
@@ -505,15 +505,15 @@ export async function runNonInteractiveOnboarding(
|
||||
runtime: daemonRuntimeRaw,
|
||||
nodePath,
|
||||
});
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
token: gatewayToken,
|
||||
launchdLabel:
|
||||
process.platform === "darwin"
|
||||
? GATEWAY_LAUNCH_AGENT_LABEL
|
||||
: undefined,
|
||||
});
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
token: gatewayToken,
|
||||
launchdLabel:
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
|
||||
: undefined,
|
||||
});
|
||||
await service.install({
|
||||
env: process.env,
|
||||
stdout: process.stdout,
|
||||
|
||||
@@ -119,7 +119,9 @@ export async function statusAllCommand(
|
||||
try {
|
||||
const service = resolveGatewayService();
|
||||
const [loaded, runtimeInfo, command] = await Promise.all([
|
||||
service.isLoaded({ env: process.env }).catch(() => false),
|
||||
service
|
||||
.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })
|
||||
.catch(() => false),
|
||||
service.readRuntime(process.env).catch(() => undefined),
|
||||
service.readCommand(process.env).catch(() => null),
|
||||
]);
|
||||
|
||||
@@ -257,7 +257,9 @@ async function getDaemonStatusSummary(): Promise<{
|
||||
try {
|
||||
const service = resolveGatewayService();
|
||||
const [loaded, runtime, command] = await Promise.all([
|
||||
service.isLoaded({ env: process.env }).catch(() => false),
|
||||
service
|
||||
.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })
|
||||
.catch(() => false),
|
||||
service.readRuntime(process.env).catch(() => undefined),
|
||||
service.readCommand(process.env).catch(() => null),
|
||||
]);
|
||||
|
||||
@@ -14,6 +14,8 @@ describe("config paths", () => {
|
||||
error: "Invalid path. Use dot notation (e.g. foo.bar).",
|
||||
});
|
||||
expect(parseConfigPath("__proto__.polluted").ok).toBe(false);
|
||||
expect(parseConfigPath("constructor.polluted").ok).toBe(false);
|
||||
expect(parseConfigPath("prototype.polluted").ok).toBe(false);
|
||||
});
|
||||
|
||||
it("sets, gets, and unsets nested values", () => {
|
||||
|
||||
@@ -39,4 +39,17 @@ describe("runtime overrides", () => {
|
||||
expect(removed.removed).toBe(true);
|
||||
expect(Object.keys(getConfigOverrides()).length).toBe(0);
|
||||
});
|
||||
|
||||
it("rejects prototype pollution paths", () => {
|
||||
const attempts = [
|
||||
"__proto__.polluted",
|
||||
"constructor.polluted",
|
||||
"prototype.polluted",
|
||||
];
|
||||
for (const path of attempts) {
|
||||
const result = setConfigOverride(path, true);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(Object.keys(getConfigOverrides()).length).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,6 +115,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.cliBackends": "CLI Backends",
|
||||
"commands.native": "Native Commands",
|
||||
"commands.text": "Text Commands",
|
||||
"commands.config": "Allow /config",
|
||||
"commands.debug": "Allow /debug",
|
||||
"commands.restart": "Allow Restart",
|
||||
"commands.useAccessGroups": "Use Access Groups",
|
||||
"ui.seamColor": "Accent Color",
|
||||
@@ -203,6 +205,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"commands.native":
|
||||
"Register native commands with connectors that support it (Discord/Slack/Telegram).",
|
||||
"commands.text": "Allow text command parsing (slash commands only).",
|
||||
"commands.config":
|
||||
"Allow /config chat command to read/write config on disk (default: false).",
|
||||
"commands.debug":
|
||||
"Allow /debug chat command for runtime-only overrides (default: false).",
|
||||
"commands.restart":
|
||||
"Allow /restart and gateway restart tool actions (default: false).",
|
||||
"commands.useAccessGroups":
|
||||
|
||||
@@ -1078,6 +1078,10 @@ export type CommandsConfig = {
|
||||
native?: boolean;
|
||||
/** Enable text command parsing (default: true). */
|
||||
text?: boolean;
|
||||
/** Allow /config command (default: false). */
|
||||
config?: boolean;
|
||||
/** Allow /debug command (default: false). */
|
||||
debug?: boolean;
|
||||
/** Allow restart commands/tools (default: false). */
|
||||
restart?: boolean;
|
||||
/** Enforce access-group allowlists/policies for commands (default: true). */
|
||||
|
||||
@@ -685,6 +685,8 @@ const CommandsSchema = z
|
||||
.object({
|
||||
native: z.boolean().optional(),
|
||||
text: z.boolean().optional(),
|
||||
config: z.boolean().optional(),
|
||||
debug: z.boolean().optional(),
|
||||
restart: z.boolean().optional(),
|
||||
useAccessGroups: z.boolean().optional(),
|
||||
})
|
||||
|
||||
168
src/daemon/constants.test.ts
Normal file
168
src/daemon/constants.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
|
||||
describe("resolveGatewayLaunchAgentLabel", () => {
|
||||
it("returns default label when no profile is set", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel();
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
expect(result).toBe("com.clawdbot.gateway");
|
||||
});
|
||||
|
||||
it("returns default label when profile is undefined", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel(undefined);
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns default label when profile is 'default'", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("default");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns default label when profile is 'Default' (case-insensitive)", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("Default");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns profile-specific label when profile is set", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("dev");
|
||||
expect(result).toBe("com.clawdbot.dev");
|
||||
});
|
||||
|
||||
it("returns profile-specific label for custom profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("work");
|
||||
expect(result).toBe("com.clawdbot.work");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel(" staging ");
|
||||
expect(result).toBe("com.clawdbot.staging");
|
||||
});
|
||||
|
||||
it("returns default label for empty string profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns default label for whitespace-only profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel(" ");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGatewaySystemdServiceName", () => {
|
||||
it("returns default service name when no profile is set", () => {
|
||||
const result = resolveGatewaySystemdServiceName();
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
expect(result).toBe("clawdbot-gateway");
|
||||
});
|
||||
|
||||
it("returns default service name when profile is undefined", () => {
|
||||
const result = resolveGatewaySystemdServiceName(undefined);
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
|
||||
it("returns default service name when profile is 'default'", () => {
|
||||
const result = resolveGatewaySystemdServiceName("default");
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
|
||||
it("returns default service name when profile is 'DEFAULT' (case-insensitive)", () => {
|
||||
const result = resolveGatewaySystemdServiceName("DEFAULT");
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
|
||||
it("returns profile-specific service name when profile is set", () => {
|
||||
const result = resolveGatewaySystemdServiceName("dev");
|
||||
expect(result).toBe("clawdbot-gateway-dev");
|
||||
});
|
||||
|
||||
it("returns profile-specific service name for custom profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName("production");
|
||||
expect(result).toBe("clawdbot-gateway-production");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName(" test ");
|
||||
expect(result).toBe("clawdbot-gateway-test");
|
||||
});
|
||||
|
||||
it("returns default service name for empty string profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName("");
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGatewayWindowsTaskName", () => {
|
||||
it("returns default task name when no profile is set", () => {
|
||||
const result = resolveGatewayWindowsTaskName();
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
expect(result).toBe("Clawdbot Gateway");
|
||||
});
|
||||
|
||||
it("returns default task name when profile is undefined", () => {
|
||||
const result = resolveGatewayWindowsTaskName(undefined);
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
|
||||
it("returns default task name when profile is 'default'", () => {
|
||||
const result = resolveGatewayWindowsTaskName("default");
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
|
||||
it("returns default task name when profile is 'DeFaUlT' (case-insensitive)", () => {
|
||||
const result = resolveGatewayWindowsTaskName("DeFaUlT");
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
|
||||
it("returns profile-specific task name when profile is set", () => {
|
||||
const result = resolveGatewayWindowsTaskName("dev");
|
||||
expect(result).toBe("Clawdbot Gateway (dev)");
|
||||
});
|
||||
|
||||
it("returns profile-specific task name for custom profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName("work");
|
||||
expect(result).toBe("Clawdbot Gateway (work)");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName(" ci ");
|
||||
expect(result).toBe("Clawdbot Gateway (ci)");
|
||||
});
|
||||
|
||||
it("returns default task name for empty string profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName("");
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatGatewayServiceDescription", () => {
|
||||
it("returns default description when no profile/version", () => {
|
||||
expect(formatGatewayServiceDescription()).toBe("Clawdbot Gateway");
|
||||
});
|
||||
|
||||
it("includes profile when set", () => {
|
||||
expect(
|
||||
formatGatewayServiceDescription({ profile: "work" }),
|
||||
).toBe("Clawdbot Gateway (profile: work)");
|
||||
});
|
||||
|
||||
it("includes version when set", () => {
|
||||
expect(
|
||||
formatGatewayServiceDescription({ version: "2026.1.10" }),
|
||||
).toBe("Clawdbot Gateway (v2026.1.10)");
|
||||
});
|
||||
|
||||
it("includes profile and version when set", () => {
|
||||
expect(
|
||||
formatGatewayServiceDescription({ profile: "dev", version: "1.2.3" }),
|
||||
).toBe("Clawdbot Gateway (profile: dev, v1.2.3)");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
export const GATEWAY_LAUNCH_AGENT_LABEL = "com.clawdbot.gateway";
|
||||
export const GATEWAY_SYSTEMD_SERVICE_NAME = "clawdbot-gateway";
|
||||
export const GATEWAY_WINDOWS_TASK_NAME = "Clawdbot Gateway";
|
||||
export const GATEWAY_SERVICE_MARKER = "clawdbot";
|
||||
export const GATEWAY_SERVICE_KIND = "gateway";
|
||||
export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = [
|
||||
"com.steipete.clawdbot.gateway",
|
||||
"com.steipete.clawdis.gateway",
|
||||
@@ -8,3 +10,46 @@ export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = [
|
||||
];
|
||||
export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES = ["clawdis-gateway"];
|
||||
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES = ["Clawdis Gateway"];
|
||||
|
||||
export function resolveGatewayLaunchAgentLabel(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
}
|
||||
return `com.clawdbot.${trimmed}`;
|
||||
}
|
||||
|
||||
function normalizeGatewayProfile(profile?: string): string | null {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function resolveGatewaySystemdServiceName(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_SYSTEMD_SERVICE_NAME;
|
||||
}
|
||||
return `clawdbot-gateway-${trimmed}`;
|
||||
}
|
||||
|
||||
export function resolveGatewayWindowsTaskName(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_WINDOWS_TASK_NAME;
|
||||
}
|
||||
return `Clawdbot Gateway (${trimmed})`;
|
||||
}
|
||||
|
||||
export function formatGatewayServiceDescription(params?: {
|
||||
profile?: string;
|
||||
version?: string;
|
||||
}): string {
|
||||
const profile = normalizeGatewayProfile(params?.profile);
|
||||
const version = params?.version?.trim();
|
||||
const parts: string[] = [];
|
||||
if (profile) parts.push(`profile: ${profile}`);
|
||||
if (version) parts.push(`v${version}`);
|
||||
if (parts.length === 0) return "Clawdbot Gateway";
|
||||
return `Clawdbot Gateway (${parts.join(", ")})`;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
GATEWAY_SERVICE_KIND,
|
||||
GATEWAY_SERVICE_MARKER,
|
||||
LEGACY_GATEWAY_LAUNCH_AGENT_LABELS,
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
|
||||
LEGACY_GATEWAY_WINDOWS_TASK_NAMES,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
|
||||
export type ExtraGatewayService = {
|
||||
@@ -26,20 +28,32 @@ export type FindExtraGatewayServicesOptions = {
|
||||
const EXTRA_MARKERS = ["clawdbot", "clawdis"];
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export function renderGatewayServiceCleanupHints(): string[] {
|
||||
export function renderGatewayServiceCleanupHints(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
): string[] {
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
case "darwin": {
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
return [
|
||||
`launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
`rm ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`,
|
||||
`launchctl bootout gui/$UID/${label}`,
|
||||
`rm ~/Library/LaunchAgents/${label}.plist`,
|
||||
];
|
||||
case "linux":
|
||||
}
|
||||
case "linux": {
|
||||
const unit = resolveGatewaySystemdServiceName(profile);
|
||||
return [
|
||||
`systemctl --user disable --now ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||
`rm ~/.config/systemd/user/${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||
`systemctl --user disable --now ${unit}.service`,
|
||||
`rm ~/.config/systemd/user/${unit}.service`,
|
||||
];
|
||||
case "win32":
|
||||
return [`schtasks /Delete /TN "${GATEWAY_WINDOWS_TASK_NAME}" /F`];
|
||||
}
|
||||
case "win32": {
|
||||
const task = resolveGatewayWindowsTaskName(profile);
|
||||
return [`schtasks /Delete /TN "${task}" /F`];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -56,6 +70,42 @@ function containsMarker(content: string): boolean {
|
||||
return EXTRA_MARKERS.some((marker) => lower.includes(marker));
|
||||
}
|
||||
|
||||
function hasGatewayServiceMarker(content: string): boolean {
|
||||
const lower = content.toLowerCase();
|
||||
return (
|
||||
lower.includes("clawdbot_service_marker") &&
|
||||
lower.includes(GATEWAY_SERVICE_MARKER.toLowerCase()) &&
|
||||
lower.includes("clawdbot_service_kind") &&
|
||||
lower.includes(GATEWAY_SERVICE_KIND.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
function isClawdbotGatewayLaunchdService(
|
||||
label: string,
|
||||
contents: string,
|
||||
): boolean {
|
||||
if (hasGatewayServiceMarker(contents)) return true;
|
||||
const lowerContents = contents.toLowerCase();
|
||||
if (!lowerContents.includes("gateway")) return false;
|
||||
return label.startsWith("com.clawdbot.");
|
||||
}
|
||||
|
||||
function isClawdbotGatewaySystemdService(
|
||||
name: string,
|
||||
contents: string,
|
||||
): boolean {
|
||||
if (hasGatewayServiceMarker(contents)) return true;
|
||||
if (!name.startsWith("clawdbot-gateway")) return false;
|
||||
return contents.toLowerCase().includes("gateway");
|
||||
}
|
||||
|
||||
function isClawdbotGatewayTaskName(name: string): boolean {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
const defaultName = resolveGatewayWindowsTaskName().toLowerCase();
|
||||
return normalized === defaultName || normalized.startsWith("clawdbot gateway");
|
||||
}
|
||||
|
||||
function tryExtractPlistLabel(contents: string): string | null {
|
||||
const match = contents.match(
|
||||
/<key>Label<\/key>\s*<string>([\s\S]*?)<\/string>/i,
|
||||
@@ -66,14 +116,14 @@ function tryExtractPlistLabel(contents: string): string | null {
|
||||
|
||||
function isIgnoredLaunchdLabel(label: string): boolean {
|
||||
return (
|
||||
label === GATEWAY_LAUNCH_AGENT_LABEL ||
|
||||
label === resolveGatewayLaunchAgentLabel() ||
|
||||
LEGACY_GATEWAY_LAUNCH_AGENT_LABELS.includes(label)
|
||||
);
|
||||
}
|
||||
|
||||
function isIgnoredSystemdName(name: string): boolean {
|
||||
return (
|
||||
name === GATEWAY_SYSTEMD_SERVICE_NAME ||
|
||||
name === resolveGatewaySystemdServiceName() ||
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES.includes(name)
|
||||
);
|
||||
}
|
||||
@@ -104,6 +154,7 @@ async function scanLaunchdDir(params: {
|
||||
if (!containsMarker(contents)) continue;
|
||||
const label = tryExtractPlistLabel(contents) ?? labelFromName;
|
||||
if (isIgnoredLaunchdLabel(label)) continue;
|
||||
if (isClawdbotGatewayLaunchdService(label, contents)) continue;
|
||||
results.push({
|
||||
platform: "darwin",
|
||||
label,
|
||||
@@ -139,6 +190,7 @@ async function scanSystemdDir(params: {
|
||||
continue;
|
||||
}
|
||||
if (!containsMarker(contents)) continue;
|
||||
if (isClawdbotGatewaySystemdService(name, contents)) continue;
|
||||
results.push({
|
||||
platform: "linux",
|
||||
label: entry,
|
||||
@@ -302,7 +354,7 @@ export async function findExtraGatewayServices(
|
||||
for (const task of tasks) {
|
||||
const name = task.name.trim();
|
||||
if (!name) continue;
|
||||
if (name === GATEWAY_WINDOWS_TASK_NAME) continue;
|
||||
if (isClawdbotGatewayTaskName(name)) continue;
|
||||
if (LEGACY_GATEWAY_WINDOWS_TASK_NAMES.includes(name)) continue;
|
||||
const lowerName = name.toLowerCase();
|
||||
const lowerCommand = task.taskToRun?.toLowerCase() ?? "";
|
||||
|
||||
@@ -7,6 +7,8 @@ import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
LEGACY_GATEWAY_LAUNCH_AGENT_LABELS,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
} from "./constants.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
@@ -34,7 +36,10 @@ function resolveLaunchAgentPlistPathForLabel(
|
||||
export function resolveLaunchAgentPlistPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
return resolveLaunchAgentPlistPathForLabel(env, GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
return resolveLaunchAgentPlistPathForLabel(env, label);
|
||||
}
|
||||
|
||||
export function resolveGatewayLogPaths(
|
||||
@@ -162,6 +167,7 @@ export async function readLaunchAgentProgramArguments(
|
||||
|
||||
export function buildLaunchAgentPlist({
|
||||
label = GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
comment,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
stdoutPath,
|
||||
@@ -169,6 +175,7 @@ export function buildLaunchAgentPlist({
|
||||
environment,
|
||||
}: {
|
||||
label?: string;
|
||||
comment?: string;
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
stdoutPath: string;
|
||||
@@ -183,6 +190,12 @@ export function buildLaunchAgentPlist({
|
||||
<key>WorkingDirectory</key>
|
||||
<string>${plistEscape(workingDirectory)}</string>`
|
||||
: "";
|
||||
const commentXml =
|
||||
comment && comment.trim()
|
||||
? `
|
||||
<key>Comment</key>
|
||||
<string>${plistEscape(comment.trim())}</string>`
|
||||
: "";
|
||||
const envXml = renderEnvDict(environment);
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
@@ -190,6 +203,7 @@ export function buildLaunchAgentPlist({
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>${plistEscape(label)}</string>
|
||||
${commentXml}
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
@@ -271,9 +285,9 @@ export function parseLaunchctlPrint(output: string): LaunchctlPrintInfo {
|
||||
return info;
|
||||
}
|
||||
|
||||
export async function isLaunchAgentLoaded(): Promise<boolean> {
|
||||
export async function isLaunchAgentLoaded(profile?: string): Promise<boolean> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
const res = await execLaunchctl(["print", `${domain}/${label}`]);
|
||||
return res.code === 0;
|
||||
}
|
||||
@@ -294,7 +308,9 @@ export async function readLaunchAgentRuntime(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
const res = await execLaunchctl(["print", `${domain}/${label}`]);
|
||||
if (res.code !== 0) {
|
||||
return {
|
||||
@@ -418,7 +434,10 @@ export async function uninstallLaunchAgent({
|
||||
|
||||
const home = resolveHomeDir(env);
|
||||
const trashDir = path.join(home, ".Trash");
|
||||
const dest = path.join(trashDir, `${GATEWAY_LAUNCH_AGENT_LABEL}.plist`);
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
const dest = path.join(trashDir, `${label}.plist`);
|
||||
try {
|
||||
await fs.mkdir(trashDir, { recursive: true });
|
||||
await fs.rename(plistPath, dest);
|
||||
@@ -443,11 +462,13 @@ function isLaunchctlNotLoaded(res: {
|
||||
|
||||
export async function stopLaunchAgent({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
const res = await execLaunchctl(["bootout", `${domain}/${label}`]);
|
||||
if (res.code !== 0 && !isLaunchctlNotLoaded(res)) {
|
||||
throw new Error(
|
||||
@@ -474,6 +495,9 @@ export async function installLaunchAgent({
|
||||
await fs.mkdir(logDir, { recursive: true });
|
||||
|
||||
const domain = resolveGuiDomain();
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
for (const legacyLabel of LEGACY_GATEWAY_LAUNCH_AGENT_LABELS) {
|
||||
const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(
|
||||
env,
|
||||
@@ -488,10 +512,17 @@ export async function installLaunchAgent({
|
||||
}
|
||||
}
|
||||
|
||||
const plistPath = resolveLaunchAgentPlistPath(env);
|
||||
const plistPath = resolveLaunchAgentPlistPathForLabel(env, label);
|
||||
await fs.mkdir(path.dirname(plistPath), { recursive: true });
|
||||
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const plist = buildLaunchAgentPlist({
|
||||
label,
|
||||
comment: description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
stdoutPath,
|
||||
@@ -508,12 +539,8 @@ export async function installLaunchAgent({
|
||||
`launchctl bootstrap failed: ${boot.stderr || boot.stdout}`.trim(),
|
||||
);
|
||||
}
|
||||
await execLaunchctl(["enable", `${domain}/${GATEWAY_LAUNCH_AGENT_LABEL}`]);
|
||||
await execLaunchctl([
|
||||
"kickstart",
|
||||
"-k",
|
||||
`${domain}/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
]);
|
||||
await execLaunchctl(["enable", `${domain}/${label}`]);
|
||||
await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
||||
|
||||
stdout.write(`${formatLine("Installed LaunchAgent", plistPath)}\n`);
|
||||
stdout.write(`${formatLine("Logs", stdoutPath)}\n`);
|
||||
@@ -522,11 +549,13 @@ export async function installLaunchAgent({
|
||||
|
||||
export async function restartLaunchAgent({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
const res = await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
|
||||
@@ -5,8 +5,9 @@ import { promisify } from "node:util";
|
||||
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
LEGACY_GATEWAY_WINDOWS_TASK_NAMES,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
@@ -28,7 +29,10 @@ function resolveTaskScriptPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
const home = resolveHomeDir(env);
|
||||
return path.join(home, ".clawdbot", "gateway.cmd");
|
||||
const profile = env.CLAWDBOT_PROFILE?.trim();
|
||||
const suffix =
|
||||
profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
return path.join(home, `.clawdbot${suffix}`, "gateway.cmd");
|
||||
}
|
||||
|
||||
function resolveLegacyTaskScriptPath(
|
||||
@@ -78,18 +82,32 @@ function parseCommandLine(value: string): string[] {
|
||||
|
||||
export async function readScheduledTaskCommand(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<{ programArguments: string[]; workingDirectory?: string } | null> {
|
||||
): Promise<{
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string>;
|
||||
} | null> {
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
try {
|
||||
const content = await fs.readFile(scriptPath, "utf8");
|
||||
let workingDirectory = "";
|
||||
let commandLine = "";
|
||||
const environment: Record<string, string> = {};
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
if (line.startsWith("@echo")) continue;
|
||||
if (line.toLowerCase().startsWith("rem ")) continue;
|
||||
if (line.toLowerCase().startsWith("set ")) continue;
|
||||
if (line.toLowerCase().startsWith("set ")) {
|
||||
const assignment = line.slice(4).trim();
|
||||
const index = assignment.indexOf("=");
|
||||
if (index > 0) {
|
||||
const key = assignment.slice(0, index).trim();
|
||||
const value = assignment.slice(index + 1).trim();
|
||||
if (key) environment[key] = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.toLowerCase().startsWith("cd /d ")) {
|
||||
workingDirectory = line
|
||||
.slice("cd /d ".length)
|
||||
@@ -104,6 +122,7 @@ export async function readScheduledTaskCommand(
|
||||
return {
|
||||
programArguments: parseCommandLine(commandLine),
|
||||
...(workingDirectory ? { workingDirectory } : {}),
|
||||
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -129,15 +148,20 @@ export function parseSchtasksQuery(output: string): ScheduledTaskInfo {
|
||||
}
|
||||
|
||||
function buildTaskScript({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
}: {
|
||||
description?: string;
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string | undefined>;
|
||||
}): string {
|
||||
const lines: string[] = ["@echo off"];
|
||||
if (description?.trim()) {
|
||||
lines.push(`rem ${description.trim()}`);
|
||||
}
|
||||
if (workingDirectory) {
|
||||
lines.push(`cd /d ${quoteCmdArg(workingDirectory)}`);
|
||||
}
|
||||
@@ -208,13 +232,20 @@ export async function installScheduledTask({
|
||||
await assertSchtasksAvailable();
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const script = buildTaskScript({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
});
|
||||
await fs.writeFile(scriptPath, script, "utf8");
|
||||
|
||||
const taskName = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
const quotedScript = quoteCmdArg(scriptPath);
|
||||
const create = await execSchtasks([
|
||||
"/Create",
|
||||
@@ -224,7 +255,7 @@ export async function installScheduledTask({
|
||||
"/RL",
|
||||
"LIMITED",
|
||||
"/TN",
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
taskName,
|
||||
"/TR",
|
||||
quotedScript,
|
||||
]);
|
||||
@@ -234,10 +265,8 @@ export async function installScheduledTask({
|
||||
);
|
||||
}
|
||||
|
||||
await execSchtasks(["/Run", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
stdout.write(
|
||||
`${formatLine("Installed Scheduled Task", GATEWAY_WINDOWS_TASK_NAME)}\n`,
|
||||
);
|
||||
await execSchtasks(["/Run", "/TN", taskName]);
|
||||
stdout.write(`${formatLine("Installed Scheduled Task", taskName)}\n`);
|
||||
stdout.write(`${formatLine("Task script", scriptPath)}\n`);
|
||||
return { scriptPath };
|
||||
}
|
||||
@@ -250,7 +279,8 @@ export async function uninstallScheduledTask({
|
||||
stdout: NodeJS.WritableStream;
|
||||
}): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
await execSchtasks(["/Delete", "/F", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
await execSchtasks(["/Delete", "/F", "/TN", taskName]);
|
||||
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
try {
|
||||
@@ -272,42 +302,52 @@ function isTaskNotRunning(res: {
|
||||
|
||||
export async function stopScheduledTask({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
const res = await execSchtasks(["/End", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(profile);
|
||||
const res = await execSchtasks(["/End", "/TN", taskName]);
|
||||
if (res.code !== 0 && !isTaskNotRunning(res)) {
|
||||
throw new Error(`schtasks end failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(
|
||||
`${formatLine("Stopped Scheduled Task", GATEWAY_WINDOWS_TASK_NAME)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Stopped Scheduled Task", taskName)}\n`);
|
||||
}
|
||||
|
||||
export async function restartScheduledTask({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
await execSchtasks(["/End", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const res = await execSchtasks(["/Run", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(profile);
|
||||
await execSchtasks(["/End", "/TN", taskName]);
|
||||
const res = await execSchtasks(["/Run", "/TN", taskName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(`schtasks run failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(
|
||||
`${formatLine("Restarted Scheduled Task", GATEWAY_WINDOWS_TASK_NAME)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Restarted Scheduled Task", taskName)}\n`);
|
||||
}
|
||||
|
||||
export async function isScheduledTaskInstalled(): Promise<boolean> {
|
||||
export async function isScheduledTaskInstalled(
|
||||
profile?: string,
|
||||
): Promise<boolean> {
|
||||
await assertSchtasksAvailable();
|
||||
const res = await execSchtasks(["/Query", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(profile);
|
||||
const res = await execSchtasks(["/Query", "/TN", taskName]);
|
||||
return res.code === 0;
|
||||
}
|
||||
|
||||
export async function readScheduledTaskRuntime(): Promise<GatewayServiceRuntime> {
|
||||
export async function readScheduledTaskRuntime(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
try {
|
||||
await assertSchtasksAvailable();
|
||||
} catch (err) {
|
||||
@@ -316,10 +356,11 @@ export async function readScheduledTaskRuntime(): Promise<GatewayServiceRuntime>
|
||||
detail: String(err),
|
||||
};
|
||||
}
|
||||
const taskName = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
const res = await execSchtasks([
|
||||
"/Query",
|
||||
"/TN",
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
taskName,
|
||||
"/V",
|
||||
"/FO",
|
||||
"LIST",
|
||||
|
||||
@@ -58,5 +58,23 @@ describe("buildServiceEnvironment", () => {
|
||||
}
|
||||
expect(env.CLAWDBOT_GATEWAY_PORT).toBe("18789");
|
||||
expect(env.CLAWDBOT_GATEWAY_TOKEN).toBe("secret");
|
||||
expect(env.CLAWDBOT_SERVICE_MARKER).toBe("clawdbot");
|
||||
expect(env.CLAWDBOT_SERVICE_KIND).toBe("gateway");
|
||||
expect(typeof env.CLAWDBOT_SERVICE_VERSION).toBe("string");
|
||||
expect(env.CLAWDBOT_SYSTEMD_UNIT).toBe("clawdbot-gateway.service");
|
||||
if (process.platform === "darwin") {
|
||||
expect(env.CLAWDBOT_LAUNCHD_LABEL).toBe("com.clawdbot.gateway");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses profile-specific unit and label", () => {
|
||||
const env = buildServiceEnvironment({
|
||||
env: { HOME: "/home/user", CLAWDBOT_PROFILE: "work" },
|
||||
port: 18789,
|
||||
});
|
||||
expect(env.CLAWDBOT_SYSTEMD_UNIT).toBe("clawdbot-gateway-work.service");
|
||||
if (process.platform === "darwin") {
|
||||
expect(env.CLAWDBOT_LAUNCHD_LABEL).toBe("com.clawdbot.work");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { VERSION } from "../version.js";
|
||||
import {
|
||||
GATEWAY_SERVICE_KIND,
|
||||
GATEWAY_SERVICE_MARKER,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
} from "./constants.js";
|
||||
|
||||
export type MinimalServicePathOptions = {
|
||||
platform?: NodeJS.Platform;
|
||||
extraDirs?: string[];
|
||||
@@ -59,13 +67,24 @@ export function buildServiceEnvironment(params: {
|
||||
launchdLabel?: string;
|
||||
}): Record<string, string | undefined> {
|
||||
const { env, port, token, launchdLabel } = params;
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
const resolvedLaunchdLabel =
|
||||
launchdLabel ||
|
||||
(process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(profile)
|
||||
: undefined);
|
||||
const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`;
|
||||
return {
|
||||
PATH: buildMinimalServicePath({ env }),
|
||||
CLAWDBOT_PROFILE: env.CLAWDBOT_PROFILE,
|
||||
CLAWDBOT_PROFILE: profile,
|
||||
CLAWDBOT_STATE_DIR: env.CLAWDBOT_STATE_DIR,
|
||||
CLAWDBOT_CONFIG_PATH: env.CLAWDBOT_CONFIG_PATH,
|
||||
CLAWDBOT_GATEWAY_PORT: String(port),
|
||||
CLAWDBOT_GATEWAY_TOKEN: token,
|
||||
CLAWDBOT_LAUNCHD_LABEL: launchdLabel,
|
||||
CLAWDBOT_LAUNCHD_LABEL: resolvedLaunchdLabel,
|
||||
CLAWDBOT_SYSTEMD_UNIT: systemdUnit,
|
||||
CLAWDBOT_SERVICE_MARKER: GATEWAY_SERVICE_MARKER,
|
||||
CLAWDBOT_SERVICE_KIND: GATEWAY_SERVICE_KIND,
|
||||
CLAWDBOT_SERVICE_VERSION: VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,11 +44,15 @@ export type GatewayService = {
|
||||
env: Record<string, string | undefined>;
|
||||
stdout: NodeJS.WritableStream;
|
||||
}) => Promise<void>;
|
||||
stop: (args: { stdout: NodeJS.WritableStream }) => Promise<void>;
|
||||
restart: (args: { stdout: NodeJS.WritableStream }) => Promise<void>;
|
||||
isLoaded: (args: {
|
||||
env: Record<string, string | undefined>;
|
||||
}) => Promise<boolean>;
|
||||
stop: (args: {
|
||||
profile?: string;
|
||||
stdout: NodeJS.WritableStream;
|
||||
}) => Promise<void>;
|
||||
restart: (args: {
|
||||
profile?: string;
|
||||
stdout: NodeJS.WritableStream;
|
||||
}) => Promise<void>;
|
||||
isLoaded: (args: { profile?: string }) => Promise<boolean>;
|
||||
readCommand: (env: Record<string, string | undefined>) => Promise<{
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
@@ -73,12 +77,15 @@ export function resolveGatewayService(): GatewayService {
|
||||
await uninstallLaunchAgent(args);
|
||||
},
|
||||
stop: async (args) => {
|
||||
await stopLaunchAgent(args);
|
||||
await stopLaunchAgent({ stdout: args.stdout, profile: args.profile });
|
||||
},
|
||||
restart: async (args) => {
|
||||
await restartLaunchAgent(args);
|
||||
await restartLaunchAgent({
|
||||
stdout: args.stdout,
|
||||
profile: args.profile,
|
||||
});
|
||||
},
|
||||
isLoaded: async () => isLaunchAgentLoaded(),
|
||||
isLoaded: async (args) => isLaunchAgentLoaded(args.profile),
|
||||
readCommand: readLaunchAgentProgramArguments,
|
||||
readRuntime: readLaunchAgentRuntime,
|
||||
};
|
||||
@@ -96,14 +103,17 @@ export function resolveGatewayService(): GatewayService {
|
||||
await uninstallSystemdService(args);
|
||||
},
|
||||
stop: async (args) => {
|
||||
await stopSystemdService(args);
|
||||
await stopSystemdService({ stdout: args.stdout, profile: args.profile });
|
||||
},
|
||||
restart: async (args) => {
|
||||
await restartSystemdService(args);
|
||||
await restartSystemdService({
|
||||
stdout: args.stdout,
|
||||
profile: args.profile,
|
||||
});
|
||||
},
|
||||
isLoaded: async () => isSystemdServiceEnabled(),
|
||||
isLoaded: async (args) => isSystemdServiceEnabled(args.profile),
|
||||
readCommand: readSystemdServiceExecStart,
|
||||
readRuntime: async () => await readSystemdServiceRuntime(),
|
||||
readRuntime: async (env) => await readSystemdServiceRuntime(env),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,14 +129,17 @@ export function resolveGatewayService(): GatewayService {
|
||||
await uninstallScheduledTask(args);
|
||||
},
|
||||
stop: async (args) => {
|
||||
await stopScheduledTask(args);
|
||||
await stopScheduledTask({ stdout: args.stdout, profile: args.profile });
|
||||
},
|
||||
restart: async (args) => {
|
||||
await restartScheduledTask(args);
|
||||
await restartScheduledTask({
|
||||
stdout: args.stdout,
|
||||
profile: args.profile,
|
||||
});
|
||||
},
|
||||
isLoaded: async () => isScheduledTaskInstalled(),
|
||||
isLoaded: async (args) => isScheduledTaskInstalled(args.profile),
|
||||
readCommand: readScheduledTaskCommand,
|
||||
readRuntime: async () => await readScheduledTaskRuntime(),
|
||||
readRuntime: async (env) => await readScheduledTaskRuntime(env),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import { promisify } from "node:util";
|
||||
import { runCommandWithTimeout, runExec } from "../process/exec.js";
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewaySystemdServiceName,
|
||||
} from "./constants.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
@@ -33,10 +34,22 @@ function resolveSystemdUnitPathForName(
|
||||
return path.join(home, ".config", "systemd", "user", `${name}.service`);
|
||||
}
|
||||
|
||||
function resolveSystemdServiceName(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
const override = env.CLAWDBOT_SYSTEMD_UNIT?.trim();
|
||||
if (override) {
|
||||
return override.endsWith(".service")
|
||||
? override.slice(0, -".service".length)
|
||||
: override;
|
||||
}
|
||||
return resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
}
|
||||
|
||||
function resolveSystemdUnitPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
return resolveSystemdUnitPathForName(env, GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
return resolveSystemdUnitPathForName(env, resolveSystemdServiceName(env));
|
||||
}
|
||||
|
||||
export function resolveSystemdUserUnitPath(
|
||||
@@ -137,22 +150,25 @@ function renderEnvLines(
|
||||
}
|
||||
|
||||
function buildSystemdUnit({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
}: {
|
||||
description?: string;
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string | undefined>;
|
||||
}): string {
|
||||
const execStart = programArguments.map(systemdEscapeArg).join(" ");
|
||||
const descriptionLine = `Description=${description?.trim() || "Clawdbot Gateway"}`;
|
||||
const workingDirLine = workingDirectory
|
||||
? `WorkingDirectory=${systemdEscapeArg(workingDirectory)}`
|
||||
: null;
|
||||
const envLines = renderEnvLines(environment);
|
||||
return [
|
||||
"[Unit]",
|
||||
"Description=Clawdbot Gateway",
|
||||
descriptionLine,
|
||||
"After=network-online.target",
|
||||
"Wants=network-online.target",
|
||||
"",
|
||||
@@ -387,14 +403,21 @@ export async function installSystemdService({
|
||||
|
||||
const unitPath = resolveSystemdUnitPath(env);
|
||||
await fs.mkdir(path.dirname(unitPath), { recursive: true });
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const unit = buildSystemdUnit({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
});
|
||||
await fs.writeFile(unitPath, unit, "utf8");
|
||||
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const reload = await execSystemctl(["--user", "daemon-reload"]);
|
||||
if (reload.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -428,7 +451,8 @@ export async function uninstallSystemdService({
|
||||
stdout: NodeJS.WritableStream;
|
||||
}): Promise<void> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
const unitName = `${serviceName}.service`;
|
||||
await execSystemctl(["--user", "disable", "--now", unitName]);
|
||||
|
||||
const unitPath = resolveSystemdUnitPath(env);
|
||||
@@ -442,11 +466,14 @@ export async function uninstallSystemdService({
|
||||
|
||||
export async function stopSystemdService({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(profile);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "stop", unitName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -458,11 +485,14 @@ export async function stopSystemdService({
|
||||
|
||||
export async function restartSystemdService({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(profile);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "restart", unitName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -472,14 +502,22 @@ export async function restartSystemdService({
|
||||
stdout.write(`${formatLine("Restarted systemd service", unitName)}\n`);
|
||||
}
|
||||
|
||||
export async function isSystemdServiceEnabled(): Promise<boolean> {
|
||||
export async function isSystemdServiceEnabled(
|
||||
profile?: string,
|
||||
): Promise<boolean> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(profile);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "is-enabled", unitName]);
|
||||
return res.code === 0;
|
||||
}
|
||||
|
||||
export async function readSystemdServiceRuntime(): Promise<GatewayServiceRuntime> {
|
||||
export async function readSystemdServiceRuntime(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
try {
|
||||
await assertSystemdAvailable();
|
||||
} catch (err) {
|
||||
@@ -488,7 +526,8 @@ export async function readSystemdServiceRuntime(): Promise<GatewayServiceRuntime
|
||||
detail: String(err),
|
||||
};
|
||||
}
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveSystemdServiceName(env);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl([
|
||||
"--user",
|
||||
"show",
|
||||
|
||||
@@ -26,7 +26,7 @@ import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import {
|
||||
buildCommandText,
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
shouldHandleTextCommands,
|
||||
} from "../auto-reply/commands-registry.js";
|
||||
import {
|
||||
@@ -416,7 +416,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
throw new Error("Failed to resolve Discord application id");
|
||||
}
|
||||
|
||||
const commandSpecs = nativeEnabled ? listNativeCommandSpecs() : [];
|
||||
const commandSpecs = nativeEnabled
|
||||
? listNativeCommandSpecsForConfig(cfg)
|
||||
: [];
|
||||
const commands = commandSpecs.map((spec) =>
|
||||
createDiscordNativeCommand({
|
||||
command: spec,
|
||||
@@ -928,7 +930,7 @@ export function createDiscordMessageHandler(params: {
|
||||
!wasMentioned &&
|
||||
!hasAnyMention &&
|
||||
commandAuthorized &&
|
||||
hasControlCommand(baseText);
|
||||
hasControlCommand(baseText, cfg);
|
||||
const effectiveWasMentioned = wasMentioned || shouldBypassMention;
|
||||
const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
|
||||
if (isGuildMessage && shouldRequireMention) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
} from "../daemon/constants.js";
|
||||
|
||||
export type RestartAttempt = {
|
||||
@@ -41,9 +41,11 @@ function formatSpawnDetail(result: {
|
||||
return "unknown error";
|
||||
}
|
||||
|
||||
function normalizeSystemdUnit(raw?: string): string {
|
||||
function normalizeSystemdUnit(raw?: string, profile?: string): string {
|
||||
const unit = raw?.trim();
|
||||
if (!unit) return `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
if (!unit) {
|
||||
return `${resolveGatewaySystemdServiceName(profile)}.service`;
|
||||
}
|
||||
return unit.endsWith(".service") ? unit : `${unit}.service`;
|
||||
}
|
||||
|
||||
@@ -54,7 +56,10 @@ export function triggerClawdbotRestart(): RestartAttempt {
|
||||
const tried: string[] = [];
|
||||
if (process.platform !== "darwin") {
|
||||
if (process.platform === "linux") {
|
||||
const unit = normalizeSystemdUnit(process.env.CLAWDBOT_SYSTEMD_UNIT);
|
||||
const unit = normalizeSystemdUnit(
|
||||
process.env.CLAWDBOT_SYSTEMD_UNIT,
|
||||
process.env.CLAWDBOT_PROFILE,
|
||||
);
|
||||
const userArgs = ["--user", "restart", unit];
|
||||
tried.push(`systemctl ${userArgs.join(" ")}`);
|
||||
const userRestart = spawnSync("systemctl", userArgs, {
|
||||
@@ -87,7 +92,8 @@ export function triggerClawdbotRestart(): RestartAttempt {
|
||||
}
|
||||
|
||||
const label =
|
||||
process.env.CLAWDBOT_LAUNCHD_LABEL || GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
process.env.CLAWDBOT_LAUNCHD_LABEL ||
|
||||
resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE);
|
||||
const uid =
|
||||
typeof process.getuid === "function" ? process.getuid() : undefined;
|
||||
const target = uid !== undefined ? `gui/${uid}/${label}` : label;
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import {
|
||||
buildCommandText,
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
shouldHandleTextCommands,
|
||||
} from "../auto-reply/commands-registry.js";
|
||||
import {
|
||||
@@ -891,7 +891,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
!wasMentioned &&
|
||||
!hasAnyMention &&
|
||||
commandAuthorized &&
|
||||
hasControlCommand(message.text ?? "");
|
||||
hasControlCommand(message.text ?? "", cfg);
|
||||
const effectiveWasMentioned = wasMentioned || shouldBypassMention;
|
||||
const canDetectMention = Boolean(botUserId) || mentionRegexes.length > 0;
|
||||
if (
|
||||
@@ -1945,7 +1945,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
};
|
||||
|
||||
const nativeCommands =
|
||||
cfg.commands?.native === true ? listNativeCommandSpecs() : [];
|
||||
cfg.commands?.native === true ? listNativeCommandSpecsForConfig(cfg) : [];
|
||||
if (nativeCommands.length > 0) {
|
||||
for (const command of nativeCommands) {
|
||||
app.command(
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import {
|
||||
buildCommandText,
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
} from "../auto-reply/commands-registry.js";
|
||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||
import { resolveTelegramDraftStreamingChunking } from "../auto-reply/reply/block-streaming.js";
|
||||
@@ -557,7 +557,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
!wasMentioned &&
|
||||
!hasAnyMention &&
|
||||
commandAuthorized &&
|
||||
hasControlCommand(msg.text ?? msg.caption ?? "");
|
||||
hasControlCommand(msg.text ?? msg.caption ?? "", cfg);
|
||||
const effectiveWasMentioned = wasMentioned || shouldBypassMention;
|
||||
const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
|
||||
if (isGroup && requireMention && canDetectMention) {
|
||||
@@ -907,7 +907,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
}
|
||||
};
|
||||
|
||||
const nativeCommands = nativeEnabled ? listNativeCommandSpecs() : [];
|
||||
const nativeCommands = nativeEnabled
|
||||
? listNativeCommandSpecsForConfig(cfg)
|
||||
: [];
|
||||
if (nativeCommands.length > 0) {
|
||||
bot.api
|
||||
.setMyCommands(
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
resolveGatewayPort,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
@@ -627,7 +627,9 @@ export async function runOnboardingWizard(
|
||||
);
|
||||
}
|
||||
const service = resolveGatewayService();
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
const loaded = await service.isLoaded({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
});
|
||||
if (loaded) {
|
||||
const action = (await prompter.select({
|
||||
message: "Gateway service already installed",
|
||||
@@ -638,7 +640,10 @@ export async function runOnboardingWizard(
|
||||
],
|
||||
})) as "restart" | "reinstall" | "skip";
|
||||
if (action === "restart") {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
await service.restart({
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
stdout: process.stdout,
|
||||
});
|
||||
} else if (action === "reinstall") {
|
||||
await service.uninstall({ env: process.env, stdout: process.stdout });
|
||||
}
|
||||
@@ -646,7 +651,9 @@ export async function runOnboardingWizard(
|
||||
|
||||
if (
|
||||
!loaded ||
|
||||
(loaded && (await service.isLoaded({ env: process.env })) === false)
|
||||
(loaded &&
|
||||
(await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })) ===
|
||||
false)
|
||||
) {
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
@@ -668,7 +675,7 @@ export async function runOnboardingWizard(
|
||||
token: gatewayToken,
|
||||
launchdLabel:
|
||||
process.platform === "darwin"
|
||||
? GATEWAY_LAUNCH_AGENT_LABEL
|
||||
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
|
||||
: undefined,
|
||||
});
|
||||
await service.install({
|
||||
|
||||
Reference in New Issue
Block a user