refactor: normalize cli command hints

This commit is contained in:
Peter Steinberger 2026-01-20 07:42:21 +00:00
parent 11b9b6dba5
commit 6d5195c890
106 changed files with 521 additions and 220 deletions

View File

@ -117,8 +117,8 @@ Send these as standalone messages so they register.
``` ```
## Inspecting ## Inspecting
- `pnpm clawdbot status` — shows store path and recent sessions. - `clawdbot status` — shows store path and recent sessions.
- `pnpm clawdbot sessions --json` — dumps every entry (filter with `--active <minutes>`). - `clawdbot sessions --json` — dumps every entry (filter with `--active <minutes>`).
- `clawdbot gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access). - `clawdbot gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access).
- Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs). - Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
- Send `/context list` or `/context detail` to see whats in the system prompt and injected workspace files (and the biggest context contributors). - Send `/context list` or `/context detail` to see whats in the system prompt and injected workspace files (and the biggest context contributors).

View File

@ -48,7 +48,7 @@ node --import tsx scripts/repro/tsx-name-repro.ts
## Regression history ## Regression history
- `2871657e` (2026-01-06): scripts changed from Bun to tsx to make Bun optional. - `2871657e` (2026-01-06): scripts changed from Bun to tsx to make Bun optional.
- Before that (Bun path), `pnpm clawdbot status` and `gateway:watch` worked. - Before that (Bun path), `clawdbot status` and `gateway:watch` worked.
## Workarounds ## Workarounds
- Use Bun for dev scripts (current temporary revert). - Use Bun for dev scripts (current temporary revert).

View File

@ -59,9 +59,11 @@ Recommended flow (dev profile + dev bootstrap):
```bash ```bash
pnpm gateway:dev pnpm gateway:dev
CLAWDBOT_PROFILE=dev pnpm clawdbot tui CLAWDBOT_PROFILE=dev clawdbot tui
``` ```
If you dont have a global install yet, run the CLI via `pnpm clawdbot ...`.
What this does: What this does:
1) **Profile isolation** (global `--dev`) 1) **Profile isolation** (global `--dev`)
@ -89,7 +91,7 @@ Note: `--dev` is a **global** profile flag and gets eaten by some runners.
If you need to spell it out, use the env var form: If you need to spell it out, use the env var form:
```bash ```bash
CLAWDBOT_PROFILE=dev pnpm clawdbot gateway --dev --reset CLAWDBOT_PROFILE=dev clawdbot gateway --dev --reset
``` ```
`--reset` wipes config, credentials, sessions, and the dev workspace (using `--reset` wipes config, credentials, sessions, and the dev workspace (using

View File

@ -378,7 +378,7 @@ clawdbot channels login
### Build errors on `main` — whats the standard fix path? ### Build errors on `main` — whats the standard fix path?
1) `git pull origin main && pnpm install` 1) `git pull origin main && pnpm install`
2) `pnpm clawdbot doctor` 2) `clawdbot doctor`
3) Check GitHub issues or Discord 3) Check GitHub issues or Discord
4) Temporary workaround: check out an older commit 4) Temporary workaround: check out an older commit
@ -392,7 +392,7 @@ Typical recovery:
git status # ensure youre in the repo root git status # ensure youre in the repo root
pnpm install pnpm install
pnpm build pnpm build
pnpm clawdbot doctor clawdbot doctor
clawdbot daemon restart clawdbot daemon restart
``` ```

View File

@ -123,9 +123,11 @@ cd clawdbot
pnpm install pnpm install
pnpm ui:build # auto-installs UI deps on first run pnpm ui:build # auto-installs UI deps on first run
pnpm build pnpm build
pnpm clawdbot onboard --install-daemon clawdbot onboard --install-daemon
``` ```
If you dont have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
Multi-instance quickstart (optional): Multi-instance quickstart (optional):
```bash ```bash

View File

@ -7,40 +7,43 @@ read_when:
# Install # Install
Runtime baseline: **Node >=22**. Use the installer unless you have a reason not to. It sets up the CLI and runs onboarding.
If the installer says it succeeded but you later see `clawdbot: command not found`, its usually a Node/npm PATH issue (global npm bin dir not on PATH). See the section below. ## Quick install (recommended)
## Node.js + npm (PATH sanity)
Quick diagnosis:
```bash
node -v
npm -v
npm bin -g
echo "$PATH"
```
If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell cant find global npm binaries (including `clawdbot`).
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
```bash
export PATH="/path/from/npm/bin/-g:$PATH"
```
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
## Recommended (installer script)
```bash ```bash
curl -fsSL https://clawd.bot/install.sh | bash curl -fsSL https://clawd.bot/install.sh | bash
``` ```
This installs the `clawdbot` CLI globally via npm and then starts onboarding. Windows (PowerShell):
See installer flags: ```powershell
iwr -useb https://clawd.bot/install.ps1 | iex
```
Next step (if you skipped onboarding):
```bash
clawdbot onboard --install-daemon
```
## System requirements
- **Node >=22**
- macOS, Linux, or Windows via WSL2
- `pnpm` only if you build from source
## Choose your install path
### 1) Installer script (recommended)
Installs `clawdbot` globally via npm and runs onboarding.
```bash
curl -fsSL https://clawd.bot/install.sh | bash
```
Installer flags:
```bash ```bash
curl -fsSL https://clawd.bot/install.sh | bash -s -- --help curl -fsSL https://clawd.bot/install.sh | bash -s -- --help
@ -54,7 +57,60 @@ Non-interactive (skip onboarding):
curl -fsSL https://clawd.bot/install.sh | bash -s -- --no-onboard curl -fsSL https://clawd.bot/install.sh | bash -s -- --no-onboard
``` ```
## Install method: npm vs git ### 2) Global install (manual)
If you already have Node:
```bash
npm install -g clawdbot@latest
```
If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails to install, force prebuilt binaries:
```bash
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g clawdbot@latest
```
Or:
```bash
pnpm add -g clawdbot@latest
```
Then:
```bash
clawdbot onboard --install-daemon
```
### 3) From source (contributors/dev)
```bash
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
clawdbot onboard --install-daemon
```
Tip: if you dont have a global install yet, run repo commands via `pnpm clawdbot ...`.
### 4) Other install options
- Docker: [Docker](/install/docker)
- Nix: [Nix](/install/nix)
- Ansible: [Ansible](/install/ansible)
- Bun (CLI only): [Bun](/install/bun)
## After install
- Run onboarding: `clawdbot onboard --install-daemon`
- Quick check: `clawdbot doctor`
- Check gateway health: `clawdbot status` + `clawdbot health`
- Open the dashboard: `clawdbot dashboard`
## Install method: npm vs git (installer)
The installer supports two methods: The installer supports two methods:
@ -92,28 +148,28 @@ Equivalent env vars (useful for automation):
- `CLAWDBOT_NO_ONBOARD=1` - `CLAWDBOT_NO_ONBOARD=1`
- `SHARP_IGNORE_GLOBAL_LIBVIPS=0|1` (default: `1`; avoids `sharp` building against system libvips) - `SHARP_IGNORE_GLOBAL_LIBVIPS=0|1` (default: `1`; avoids `sharp` building against system libvips)
## Global install (manual) ## Troubleshooting: `clawdbot` not found (PATH)
If you already have Node: Quick diagnosis:
```bash ```bash
npm install -g clawdbot@latest node -v
npm -v
npm bin -g
echo "$PATH"
``` ```
If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails to install, force prebuilt binaries: If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell cant find global npm binaries (including `clawdbot`).
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
```bash ```bash
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g clawdbot@latest export PATH="/path/from/npm/bin/-g:$PATH"
``` ```
Or: Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
```bash ## Update / uninstall
pnpm add -g clawdbot@latest
```
Then: - Updates: [Updating](/install/updating)
- Uninstall: [Uninstall](/install/uninstall)
```bash
clawdbot onboard --install-daemon
```

View File

@ -118,7 +118,7 @@ Remove it with `npm rm -g clawdbot` (or `pnpm remove -g` / `bun remove -g` if yo
### Source checkout (git clone) ### Source checkout (git clone)
If you run from a repo checkout (`git clone` + `pnpm clawdbot ...` / `bun run clawdbot ...`): If you run from a repo checkout (`git clone` + `clawdbot ...` / `bun run clawdbot ...`):
1) Uninstall the gateway service **before** deleting the repo (use the easy path above or manual service removal). 1) Uninstall the gateway service **before** deleting the repo (use the easy path above or manual service removal).
2) Delete the repo directory. 2) Delete the repo directory.

View File

@ -119,12 +119,13 @@ git pull
pnpm install pnpm install
pnpm build pnpm build
pnpm ui:build # auto-installs UI deps on first run pnpm ui:build # auto-installs UI deps on first run
pnpm clawdbot doctor clawdbot doctor
pnpm clawdbot health clawdbot health
``` ```
Notes: Notes:
- `pnpm build` matters when you run the packaged `clawdbot` binary ([`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js)) or use Node to run `dist/`. - `pnpm build` matters when you run the packaged `clawdbot` binary ([`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js)) or use Node to run `dist/`.
- If you run from a repo checkout without a global install, use `pnpm clawdbot ...` for CLI commands.
- If you run directly from TypeScript (`pnpm clawdbot ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor. - If you run directly from TypeScript (`pnpm clawdbot ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor.
- Switching between global and git installs is easy: install the other flavor, then run `clawdbot doctor` so the gateway service entrypoint is rewritten to the current install. - Switching between global and git installs is easy: install the other flavor, then run `clawdbot doctor` so the gateway service entrypoint is rewritten to the current install.

View File

@ -155,7 +155,7 @@ Options:
- `--timeout <ms>`: overall discovery window (default `2000`) - `--timeout <ms>`: overall discovery window (default `2000`)
- `--json`: structured output for diffing - `--json`: structured output for diffing
Tip: compare against `pnpm clawdbot gateway discover --json` to see whether the Tip: compare against `clawdbot gateway discover --json` to see whether the
macOS apps discovery pipeline (NWBrowser + tailnet DNSSD fallback) differs from macOS apps discovery pipeline (NWBrowser + tailnet DNSSD fallback) differs from
the Node CLIs `dns-sd` based discovery. the Node CLIs `dns-sd` based discovery.

View File

@ -97,7 +97,7 @@ cd clawdbot
pnpm install pnpm install
pnpm ui:build # auto-installs UI deps on first run pnpm ui:build # auto-installs UI deps on first run
pnpm build pnpm build
pnpm clawdbot onboard clawdbot onboard
``` ```
Full guide: [Getting Started](/start/getting-started) Full guide: [Getting Started](/start/getting-started)

View File

@ -184,22 +184,25 @@ Clawdbot is a personal AI assistant you run on your own devices. It replies on t
The repo recommends running from source and using the onboarding wizard: The repo recommends running from source and using the onboarding wizard:
```bash ```bash
git clone https://github.com/clawdbot/clawdbot.git curl -fsSL https://clawd.bot/install.sh | bash
cd clawdbot clawdbot onboard --install-daemon
pnpm install
# Optional if you want built output / global linking:
pnpm build
# If the Control UI assets are missing or you want the dashboard:
pnpm ui:build # auto-installs UI deps on first run
pnpm clawdbot onboard
``` ```
The wizard can also build UI assets automatically. After onboarding, you typically run the Gateway on port **18789**. The wizard can also build UI assets automatically. After onboarding, you typically run the Gateway on port **18789**.
From source (contributors/dev):
```bash
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm build
pnpm ui:build # auto-installs UI deps on first run
clawdbot onboard
```
If you dont have a global install yet, run it via `pnpm clawdbot onboard`.
### How do I open the dashboard after onboarding? ### How do I open the dashboard after onboarding?
The wizard now opens your browser with a tokenized dashboard URL right after onboarding and also prints the full link (with token) in the summary. Keep that tab open; if it didnt launch, copy/paste the printed URL on the same machine. Tokens stay local to your host—nothing is fetched from the browser. The wizard now opens your browser with a tokenized dashboard URL right after onboarding and also prints the full link (with token) in the summary. Keep that tab open; if it didnt launch, copy/paste the printed URL on the same machine. Tokens stay local to your host—nothing is fetched from the browser.
@ -330,7 +333,7 @@ git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot cd clawdbot
pnpm install pnpm install
pnpm build pnpm build
pnpm clawdbot doctor clawdbot doctor
clawdbot daemon restart clawdbot daemon restart
``` ```

View File

@ -116,6 +116,13 @@ If a token is configured, paste it into the Control UI settings (stored as `conn
⚠️ **Bun warning (WhatsApp + Telegram):** Bun has known issues with these ⚠️ **Bun warning (WhatsApp + Telegram):** Bun has known issues with these
channels. If you use WhatsApp or Telegram, run the Gateway with **Node**. channels. If you use WhatsApp or Telegram, run the Gateway with **Node**.
## 3.5) Quick verify (2 min)
```bash
clawdbot status
clawdbot health
```
## 4) Pair + connect your first chat surface ## 4) Pair + connect your first chat surface
### WhatsApp (QR login) ### WhatsApp (QR login)
@ -158,9 +165,11 @@ cd clawdbot
pnpm install pnpm install
pnpm ui:build # auto-installs UI deps on first run pnpm ui:build # auto-installs UI deps on first run
pnpm build pnpm build
pnpm clawdbot onboard --install-daemon clawdbot onboard --install-daemon
``` ```
If you dont have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
Gateway (from this repo): Gateway (from this repo):
```bash ```bash
@ -169,15 +178,13 @@ node dist/entry.js gateway --port 18789 --verbose
## 7) Verify end-to-end ## 7) Verify end-to-end
In a new terminal: In a new terminal, send a test message:
```bash ```bash
clawdbot status
clawdbot health
clawdbot message send --target +15555550123 --message "Hello from Clawdbot" clawdbot message send --target +15555550123 --message "Hello from Clawdbot"
``` ```
If `health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent wont be able to respond without it. If `clawdbot health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent wont be able to respond without it.
Tip: `clawdbot status --all` is the best pasteable, read-only debug report. Tip: `clawdbot status --all` is the best pasteable, read-only debug report.
Health probes: `clawdbot health` (or `clawdbot status --deep`) asks the running gateway for a health snapshot. Health probes: `clawdbot health` (or `clawdbot status --deep`) asks the running gateway for a health snapshot.

View File

@ -35,9 +35,11 @@ clawdbot setup
From inside this repo, use the local CLI entry: From inside this repo, use the local CLI entry:
```bash ```bash
pnpm clawdbot setup clawdbot setup
``` ```
If you dont have a global install yet, run it via `pnpm clawdbot setup`.
## Stable workflow (macOS app first) ## Stable workflow (macOS app first)
1) Install + launch **Clawdbot.app** (menu bar). 1) Install + launch **Clawdbot.app** (menu bar).
@ -92,7 +94,7 @@ The app will attach to the running gateway on the configured port.
- Or via CLI: - Or via CLI:
```bash ```bash
pnpm clawdbot health clawdbot health
``` ```
### Common footguns ### Common footguns

View File

@ -150,8 +150,8 @@ Live tests are split into two layers so we can isolate failures:
Tip: to see what you can test on your machine (and the exact `provider/model` ids), run: Tip: to see what you can test on your machine (and the exact `provider/model` ids), run:
```bash ```bash
pnpm clawdbot models list clawdbot models list
pnpm clawdbot models list --json clawdbot models list --json
``` ```
## Live: Anthropic setup-token smoke ## Live: Anthropic setup-token smoke

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../../cli/command-format.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { normalizeProviderId } from "../model-selection.js"; import { normalizeProviderId } from "../model-selection.js";
import { listProfilesForProvider } from "./profiles.js"; import { listProfilesForProvider } from "./profiles.js";
@ -37,6 +38,6 @@ export function formatAuthDoctorHint(params: {
}`, }`,
`- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`, `- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`,
`- suggested profile: ${suggested}`, `- suggested profile: ${suggested}`,
'Fix: run "clawdbot doctor --yes"', `Fix: run "${formatCliCommand("clawdbot doctor --yes")}"`,
].join("\n"); ].join("\n");
} }

View File

@ -4,6 +4,7 @@ import { type Api, getEnvApiKey, type Model } from "@mariozechner/pi-ai";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { ModelProviderConfig } from "../config/types.js"; import type { ModelProviderConfig } from "../config/types.js";
import { getShellEnvAppliedKeys } from "../infra/shell-env.js"; import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
import { formatCliCommand } from "../cli/command-format.js";
import { import {
type AuthProfileStore, type AuthProfileStore,
ensureAuthProfileStore, ensureAuthProfileStore,
@ -103,7 +104,7 @@ export async function resolveApiKeyForProvider(params: {
[ [
`No API key found for provider "${provider}".`, `No API key found for provider "${provider}".`,
`Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`, `Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`,
"Configure auth for this agent (clawdbot agents add <id>) or copy auth-profiles.json from the main agentDir.", `Configure auth for this agent (${formatCliCommand("clawdbot agents add <id>")}) or copy auth-profiles.json from the main agentDir.`,
].join(" "), ].join(" "),
); );
} }

View File

@ -1,6 +1,7 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { DEFAULT_SANDBOX_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js"; import { DEFAULT_SANDBOX_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js";
import { readRegistry, updateRegistry } from "./registry.js"; import { readRegistry, updateRegistry } from "./registry.js";
import { computeSandboxConfigHash } from "./config-hash.js"; import { computeSandboxConfigHash } from "./config-hash.js";
@ -214,13 +215,13 @@ async function readContainerConfigHash(containerName: string): Promise<string |
function formatSandboxRecreateHint(params: { scope: SandboxConfig["scope"]; sessionKey: string }) { function formatSandboxRecreateHint(params: { scope: SandboxConfig["scope"]; sessionKey: string }) {
if (params.scope === "session") { if (params.scope === "session") {
return `clawdbot sandbox recreate --session ${params.sessionKey}`; return formatCliCommand(`clawdbot sandbox recreate --session ${params.sessionKey}`);
} }
if (params.scope === "agent") { if (params.scope === "agent") {
const agentId = resolveSandboxAgentId(params.sessionKey) ?? "main"; const agentId = resolveSandboxAgentId(params.sessionKey) ?? "main";
return `clawdbot sandbox recreate --agent ${agentId}`; return formatCliCommand(`clawdbot sandbox recreate --agent ${agentId}`);
} }
return "clawdbot sandbox recreate --all"; return formatCliCommand("clawdbot sandbox recreate --all");
} }
export async function ensureSandboxContainer(params: { export async function ensureSandboxContainer(params: {

View File

@ -2,6 +2,7 @@ import type { ClawdbotConfig } from "../../config/config.js";
import { canonicalizeMainSessionAlias, resolveAgentMainSessionKey } from "../../config/sessions.js"; import { canonicalizeMainSessionAlias, resolveAgentMainSessionKey } from "../../config/sessions.js";
import { resolveSessionAgentId } from "../agent-scope.js"; import { resolveSessionAgentId } from "../agent-scope.js";
import { expandToolGroups } from "../tool-policy.js"; import { expandToolGroups } from "../tool-policy.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { resolveSandboxConfigForAgent } from "./config.js"; import { resolveSandboxConfigForAgent } from "./config.js";
import { resolveSandboxToolPolicyForAgent } from "./tool-policy.js"; import { resolveSandboxToolPolicyForAgent } from "./tool-policy.js";
import type { SandboxConfig, SandboxToolPolicyResolved } from "./types.js"; import type { SandboxConfig, SandboxToolPolicyResolved } from "./types.js";
@ -115,7 +116,9 @@ export function formatSandboxToolPolicyBlockedMessage(params: {
if (runtime.mode === "non-main") { if (runtime.mode === "non-main") {
lines.push(`- Use main session key (direct): ${runtime.mainSessionKey}`); lines.push(`- Use main session key (direct): ${runtime.mainSessionKey}`);
} }
lines.push(`- See: clawdbot sandbox explain --session ${runtime.sessionKey}`); lines.push(
`- See: ${formatCliCommand(`clawdbot sandbox explain --session ${runtime.sessionKey}`)}`,
);
return lines.join("\n"); return lines.join("\n");
} }

View File

@ -1,5 +1,6 @@
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js"; import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { formatCliCommand } from "../cli/command-format.js";
import { listDeliverableMessageChannels } from "../utils/message-channel.js"; import { listDeliverableMessageChannels } from "../utils/message-channel.js";
import type { ResolvedTimeFormat } from "./date-time.js"; import type { ResolvedTimeFormat } from "./date-time.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
@ -124,7 +125,7 @@ function buildDocsSection(params: { docsPath?: string; isMinimal: boolean; readT
"Community: https://discord.com/invite/clawd", "Community: https://discord.com/invite/clawd",
"Find new skills: https://clawdhub.com", "Find new skills: https://clawdhub.com",
"For Clawdbot behavior, commands, config, or architecture: consult local docs first.", "For Clawdbot behavior, commands, config, or architecture: consult local docs first.",
"When diagnosing issues, run `clawdbot status` yourself when possible; only ask the user if you lack access (e.g., sandboxed).", `When diagnosing issues, run \`${formatCliCommand("clawdbot status")}\` yourself when possible; only ask the user if you lack access (e.g., sandboxed).`,
"", "",
]; ];
} }
@ -364,11 +365,11 @@ export function buildAgentSystemPrompt(params: {
"## Clawdbot CLI Quick Reference", "## Clawdbot CLI Quick Reference",
"Clawdbot is controlled via subcommands. Do not invent commands.", "Clawdbot is controlled via subcommands. Do not invent commands.",
"To manage the Gateway daemon service (start/stop/restart):", "To manage the Gateway daemon service (start/stop/restart):",
"- clawdbot daemon status", `- ${formatCliCommand("clawdbot daemon status")}`,
"- clawdbot daemon start", `- ${formatCliCommand("clawdbot daemon start")}`,
"- clawdbot daemon stop", `- ${formatCliCommand("clawdbot daemon stop")}`,
"- clawdbot daemon restart", `- ${formatCliCommand("clawdbot daemon restart")}`,
"If unsure, ask the user to run `clawdbot help` (or `clawdbot daemon --help`) and paste the output.", `If unsure, ask the user to run \`${formatCliCommand("clawdbot help")}\` (or \`${formatCliCommand("clawdbot daemon --help")}\`) and paste the output.`,
"", "",
...skillsSection, ...skillsSection,
...memorySection, ...memorySection,

View File

@ -5,7 +5,7 @@ import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import { import {
DOCTOR_NONINTERACTIVE_HINT, formatDoctorNonInteractiveHint,
type RestartSentinelPayload, type RestartSentinelPayload,
writeRestartSentinel, writeRestartSentinel,
} from "../../infra/restart-sentinel.js"; } from "../../infra/restart-sentinel.js";
@ -83,7 +83,7 @@ export function createGatewayTool(opts?: {
ts: Date.now(), ts: Date.now(),
sessionKey, sessionKey,
message: note ?? reason ?? null, message: note ?? reason ?? null,
doctorHint: DOCTOR_NONINTERACTIVE_HINT, doctorHint: formatDoctorNonInteractiveHint(),
stats: { stats: {
mode: "gateway.restart", mode: "gateway.restart",
reason, reason,

View File

@ -1,6 +1,7 @@
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { formatCliCommand } from "../../cli/command-format.js";
import type { AnyAgentTool } from "./common.js"; import type { AnyAgentTool } from "./common.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js"; import { jsonResult, readNumberParam, readStringParam } from "./common.js";
import { import {
@ -124,8 +125,7 @@ function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) {
} }
return { return {
error: "missing_brave_api_key", error: "missing_brave_api_key",
message: message: `web_search needs a Brave Search API key. Run \`${formatCliCommand("clawdbot configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`,
"web_search needs a Brave Search API key. Run `clawdbot configure --section web` to store it, or set BRAVE_API_KEY in the Gateway environment.",
docs: "https://docs.clawd.bot/tools/web", docs: "https://docs.clawd.bot/tools/web",
}; };
} }

View File

@ -4,6 +4,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { isSubagentSessionKey } from "../routing/session-key.js"; import { isSubagentSessionKey } from "../routing/session-key.js";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
export function resolveDefaultAgentWorkspaceDir( export function resolveDefaultAgentWorkspaceDir(
@ -135,7 +136,7 @@ After the user chooses, update:
- Notes - Notes
3) ~/.clawdbot/clawdbot.json 3) ~/.clawdbot/clawdbot.json
Run: clawdbot agents set-identity --workspace "<this workspace>" --from-identity Run: ${formatCliCommand('clawdbot agents set-identity --workspace "<this workspace>" --from-identity')}
If multiple agents share a host, add --agent <id>. If multiple agents share a host, add --agent <id>.
## Cleanup ## Cleanup

View File

@ -4,6 +4,7 @@ import { createExecTool } from "../../agents/bash-tools.js";
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
import { killProcessTree } from "../../agents/shell-utils.js"; import { killProcessTree } from "../../agents/shell-utils.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { logVerbose } from "../../globals.js"; import { logVerbose } from "../../globals.js";
import { clampInt } from "../../utils.js"; import { clampInt } from "../../utils.js";
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
@ -167,7 +168,9 @@ function formatElevatedUnavailableMessage(params: {
lines.push("- agents.list[].tools.elevated.enabled"); lines.push("- agents.list[].tools.elevated.enabled");
lines.push("- agents.list[].tools.elevated.allowFrom.<provider>"); lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
if (params.sessionKey) { if (params.sessionKey) {
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`); lines.push(
`See: ${formatCliCommand(`clawdbot sandbox explain --session ${params.sessionKey}`)}`,
);
} }
return lines.join("\n"); return lines.join("\n");
} }

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../../cli/command-format.js";
import type { ElevatedLevel, ReasoningLevel } from "./directives.js"; import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
export const SYSTEM_MARK = "⚙️"; export const SYSTEM_MARK = "⚙️";
@ -44,7 +45,9 @@ export function formatElevatedUnavailableText(params: {
); );
} }
if (params.sessionKey) { if (params.sessionKey) {
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`); lines.push(
`See: ${formatCliCommand(`clawdbot sandbox explain --session ${params.sessionKey}`)}`,
);
} }
return lines.join("\n"); return lines.join("\n");
} }

View File

@ -4,6 +4,7 @@ import { normalizeChannelId } from "../../channels/plugins/index.js";
import { CHAT_CHANNEL_ORDER } from "../../channels/registry.js"; import { CHAT_CHANNEL_ORDER } from "../../channels/registry.js";
import type { AgentElevatedAllowFromConfig, ClawdbotConfig } from "../../config/config.js"; import type { AgentElevatedAllowFromConfig, ClawdbotConfig } from "../../config/config.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { formatCliCommand } from "../../cli/command-format.js";
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
function normalizeAllowToken(value?: string) { function normalizeAllowToken(value?: string) {
@ -187,7 +188,9 @@ export function formatElevatedUnavailableMessage(params: {
lines.push("- agents.list[].tools.elevated.enabled"); lines.push("- agents.list[].tools.elevated.enabled");
lines.push("- agents.list[].tools.elevated.allowFrom.<provider>"); lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
if (params.sessionKey) { if (params.sessionKey) {
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`); lines.push(
`See: ${formatCliCommand(`clawdbot sandbox explain --session ${params.sessionKey}`)}`,
);
} }
return lines.join("\n"); return lines.join("\n");
} }

View File

@ -1,5 +1,6 @@
import { extractErrorCode, formatErrorMessage } from "../infra/errors.js"; import { extractErrorCode, formatErrorMessage } from "../infra/errors.js";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveBrowserConfig } from "./config.js"; import { resolveBrowserConfig } from "./config.js";
let cachedConfigToken: string | null | undefined = undefined; let cachedConfigToken: string | null | undefined = undefined;
@ -30,8 +31,7 @@ function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number):
const cause = unwrapCause(err); const cause = unwrapCause(err);
const code = extractErrorCode(cause) ?? extractErrorCode(err) ?? ""; const code = extractErrorCode(cause) ?? extractErrorCode(err) ?? "";
const hint = const hint = `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`;
"Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or `clawdbot gateway`) and try again.";
if (code === "ECONNREFUSED") { if (code === "ECONNREFUSED") {
return new Error( return new Error(

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../cli/command-format.js";
import { ensurePageState, getPageForTargetId } from "./pw-session.js"; import { ensurePageState, getPageForTargetId } from "./pw-session.js";
import { normalizeTimeoutMs } from "./pw-tools-core.shared.js"; import { normalizeTimeoutMs } from "./pw-tools-core.shared.js";
@ -65,7 +66,7 @@ export async function responseBodyViaPlaywright(opts: {
cleanup(); cleanup();
reject( reject(
new Error( new Error(
`Response not found for url pattern "${pattern}". Run 'clawdbot browser requests' to inspect recent network activity.`, `Response not found for url pattern "${pattern}". Run '${formatCliCommand("clawdbot browser requests")}' to inspect recent network activity.`,
), ),
); );
}, timeout); }, timeout);

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../../cli/command-format.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import type { ChannelPlugin } from "./types.js"; import type { ChannelPlugin } from "./types.js";
@ -13,5 +14,7 @@ export function resolveChannelDefaultAccountId<ResolvedAccount>(params: {
} }
export function formatPairingApproveHint(channelId: string): string { export function formatPairingApproveHint(channelId: string): string {
return `Approve via: clawdbot pairing list ${channelId} / clawdbot pairing approve ${channelId} <code>`; const listCmd = formatCliCommand(`clawdbot pairing list ${channelId}`);
const approveCmd = formatCliCommand(`clawdbot pairing approve ${channelId} <code>`);
return `Approve via: ${listCmd} / ${approveCmd}`;
} }

View File

@ -9,6 +9,7 @@ import {
resolveSignalAccount, resolveSignalAccount,
} from "../../../signal/accounts.js"; } from "../../../signal/accounts.js";
import { formatDocsLink } from "../../../terminal/links.js"; import { formatDocsLink } from "../../../terminal/links.js";
import { formatCliCommand } from "../../../cli/command-format.js";
import { normalizeE164 } from "../../../utils.js"; import { normalizeE164 } from "../../../utils.js";
import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { WizardPrompter } from "../../../wizard/prompts.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
@ -283,7 +284,7 @@ export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
[ [
'Link device with: signal-cli link -n "Clawdbot"', 'Link device with: signal-cli link -n "Clawdbot"',
"Scan QR in Signal → Linked Devices", "Scan QR in Signal → Linked Devices",
"Then run: clawdbot gateway call channels.status --params '{\"probe\":true}'", `Then run: ${formatCliCommand("clawdbot gateway call channels.status --params '{\"probe\":true}'")}`,
`Docs: ${formatDocsLink("/signal", "signal")}`, `Docs: ${formatDocsLink("/signal", "signal")}`,
].join("\n"), ].join("\n"),
"Signal next steps", "Signal next steps",

View File

@ -7,6 +7,7 @@ import {
resolveTelegramAccount, resolveTelegramAccount,
} from "../../../telegram/accounts.js"; } from "../../../telegram/accounts.js";
import { formatDocsLink } from "../../../terminal/links.js"; import { formatDocsLink } from "../../../terminal/links.js";
import { formatCliCommand } from "../../../cli/command-format.js";
import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { WizardPrompter } from "../../../wizard/prompts.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js"; import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
@ -46,7 +47,7 @@ async function noteTelegramTokenHelp(prompter: WizardPrompter): Promise<void> {
async function noteTelegramUserIdHelp(prompter: WizardPrompter): Promise<void> { async function noteTelegramUserIdHelp(prompter: WizardPrompter): Promise<void> {
await prompter.note( await prompter.note(
[ [
"1) DM your bot, then read from.id in `clawdbot logs --follow` (safest)", `1) DM your bot, then read from.id in \`${formatCliCommand("clawdbot logs --follow")}\` (safest)`,
"2) Or call https://api.telegram.org/bot<bot_token>/getUpdates and read message.from.id", "2) Or call https://api.telegram.org/bot<bot_token>/getUpdates and read message.from.id",
"3) Third-party: DM @userinfobot or @getidsbot", "3) Third-party: DM @userinfobot or @getidsbot",
`Docs: ${formatDocsLink("/telegram")}`, `Docs: ${formatDocsLink("/telegram")}`,

View File

@ -7,6 +7,7 @@ import type { DmPolicy } from "../../../config/types.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import type { RuntimeEnv } from "../../../runtime.js"; import type { RuntimeEnv } from "../../../runtime.js";
import { formatDocsLink } from "../../../terminal/links.js"; import { formatDocsLink } from "../../../terminal/links.js";
import { formatCliCommand } from "../../../cli/command-format.js";
import { normalizeE164 } from "../../../utils.js"; import { normalizeE164 } from "../../../utils.js";
import { import {
listWhatsAppAccountIds, listWhatsAppAccountIds,
@ -321,7 +322,10 @@ export const whatsappOnboardingAdapter: ChannelOnboardingAdapter = {
await prompter.note(`Docs: ${formatDocsLink("/whatsapp", "whatsapp")}`, "WhatsApp help"); await prompter.note(`Docs: ${formatDocsLink("/whatsapp", "whatsapp")}`, "WhatsApp help");
} }
} else if (!linked) { } else if (!linked) {
await prompter.note("Run `clawdbot channels login` later to link WhatsApp.", "WhatsApp"); await prompter.note(
`Run \`${formatCliCommand("clawdbot channels login")}\` later to link WhatsApp.`,
"WhatsApp",
);
} }
next = await promptWhatsAppAllowFrom(next, runtime, prompter, { next = await promptWhatsAppAllowFrom(next, runtime, prompter, {

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../../../cli/command-format.js";
import type { ChannelAccountSnapshot, ChannelStatusIssue } from "../types.js"; import type { ChannelAccountSnapshot, ChannelStatusIssue } from "../types.js";
import { asString, isRecord } from "./shared.js"; import { asString, isRecord } from "./shared.js";
@ -47,7 +48,7 @@ export function collectWhatsAppStatusIssues(
accountId, accountId,
kind: "auth", kind: "auth",
message: "Not linked (no WhatsApp Web session).", message: "Not linked (no WhatsApp Web session).",
fix: "Run: clawdbot channels login (scan QR on the gateway host).", fix: `Run: ${formatCliCommand("clawdbot channels login")} (scan QR on the gateway host).`,
}); });
continue; continue;
} }
@ -58,7 +59,7 @@ export function collectWhatsAppStatusIssues(
accountId, accountId,
kind: "runtime", kind: "runtime",
message: `Linked but disconnected${reconnectAttempts != null ? ` (reconnectAttempts=${reconnectAttempts})` : ""}${lastError ? `: ${lastError}` : "."}`, message: `Linked but disconnected${reconnectAttempts != null ? ` (reconnectAttempts=${reconnectAttempts})` : ""}${lastError ? `: ${lastError}` : "."}`,
fix: "Run: clawdbot doctor (or restart the gateway). If it persists, relink via channels login and check logs.", fix: `Run: ${formatCliCommand("clawdbot doctor")} (or restart the gateway). If it persists, relink via channels login and check logs.`,
}); });
} }
} }

View File

@ -11,6 +11,7 @@ import { defaultRuntime } from "../runtime.js";
import { movePathToTrash } from "../browser/trash.js"; import { movePathToTrash } from "../browser/trash.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
function bundledExtensionRootDir() { function bundledExtensionRootDir() {
const here = path.dirname(fileURLToPath(import.meta.url)); const here = path.dirname(fileURLToPath(import.meta.url));
@ -103,7 +104,7 @@ export function registerBrowserExtensionCommands(
defaultRuntime.error( defaultRuntime.error(
danger( danger(
[ [
'Chrome extension is not installed. Run: "clawdbot browser extension install"', `Chrome extension is not installed. Run: "${formatCliCommand("clawdbot browser extension install")}"`,
`Docs: ${formatDocsLink("/tools/chrome-extension", "docs.clawd.bot/tools/chrome-extension")}`, `Docs: ${formatDocsLink("/tools/chrome-extension", "docs.clawd.bot/tools/chrome-extension")}`,
].join("\n"), ].join("\n"),
), ),

View File

@ -4,6 +4,7 @@ import { danger } from "../globals.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js"; import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js";
import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js"; import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js";
import { registerBrowserDebugCommands } from "./browser-cli-debug.js"; import { registerBrowserDebugCommands } from "./browser-cli-debug.js";
@ -32,7 +33,9 @@ export function registerBrowserCli(program: Command) {
) )
.action(() => { .action(() => {
browser.outputHelp(); browser.outputHelp();
defaultRuntime.error(danger('Missing subcommand. Try: "clawdbot browser status"')); defaultRuntime.error(
danger(`Missing subcommand. Try: "${formatCliCommand("clawdbot browser status")}"`),
);
defaultRuntime.exit(1); defaultRuntime.exit(1);
}); });

16
src/cli/command-format.ts Normal file
View File

@ -0,0 +1,16 @@
import { normalizeProfileName } from "./profile-utils.js";
const CLI_PREFIX_RE = /^(?:pnpm|npm|bunx|npx)\s+clawdbot\b|^clawdbot\b/;
const PROFILE_FLAG_RE = /\b--profile\b/;
const DEV_FLAG_RE = /\b--dev\b/;
export function formatCliCommand(
command: string,
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
): string {
const profile = normalizeProfileName(env.CLAWDBOT_PROFILE);
if (!profile) return command;
if (!CLI_PREFIX_RE.test(command)) return command;
if (PROFILE_FLAG_RE.test(command) || DEV_FLAG_RE.test(command)) return command;
return command.replace(CLI_PREFIX_RE, (match) => `${match} --profile ${profile}`);
}

View File

@ -5,6 +5,7 @@ import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
import { danger, info } from "../globals.js"; import { danger, info } from "../globals.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { formatCliCommand } from "./command-format.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
type PathSegment = string; type PathSegment = string;
@ -171,7 +172,7 @@ async function loadValidConfig() {
for (const issue of snapshot.issues) { for (const issue of snapshot.issues) {
defaultRuntime.error(`- ${issue.path || "<root>"}: ${issue.message}`); defaultRuntime.error(`- ${issue.path || "<root>"}: ${issue.message}`);
} }
defaultRuntime.error("Run `clawdbot doctor` to repair, then retry."); defaultRuntime.error(`Run \`${formatCliCommand("clawdbot doctor")}\` to repair, then retry.`);
defaultRuntime.exit(1); defaultRuntime.exit(1);
return snapshot; return snapshot;
} }

View File

@ -7,6 +7,7 @@ import { loadConfig, resolveGatewayPort } from "../../config/config.js";
import { resolveIsNixMode } from "../../config/paths.js"; import { resolveIsNixMode } from "../../config/paths.js";
import { resolveGatewayService } from "../../daemon/service.js"; import { resolveGatewayService } from "../../daemon/service.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js"; import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
import { parsePort } from "./shared.js"; import { parsePort } from "./shared.js";
import type { DaemonInstallOptions } from "./types.js"; import type { DaemonInstallOptions } from "./types.js";
@ -82,7 +83,9 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
}); });
if (!json) { if (!json) {
defaultRuntime.log(`Gateway service already ${service.loadedText}.`); defaultRuntime.log(`Gateway service already ${service.loadedText}.`);
defaultRuntime.log("Reinstall with: clawdbot daemon install --force"); defaultRuntime.log(
`Reinstall with: ${formatCliCommand("clawdbot daemon install --force")}`,
);
} }
return; return;
} }

View File

@ -5,6 +5,7 @@ import {
} from "../../daemon/constants.js"; } from "../../daemon/constants.js";
import { resolveGatewayLogPaths } from "../../daemon/launchd.js"; import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
import { getResolvedLoggerSettings } from "../../logging.js"; import { getResolvedLoggerSettings } from "../../logging.js";
import { formatCliCommand } from "../command-format.js";
export function parsePort(raw: unknown): number | null { export function parsePort(raw: unknown): number | null {
if (raw === undefined || raw === null) return null; if (raw === undefined || raw === null) return null;
@ -122,7 +123,7 @@ export function renderRuntimeHints(
} }
})(); })();
if (runtime.missingUnit) { if (runtime.missingUnit) {
hints.push("Service not installed. Run: clawdbot daemon install"); hints.push(`Service not installed. Run: ${formatCliCommand("clawdbot daemon install", env)}`);
if (fileLog) hints.push(`File logs: ${fileLog}`); if (fileLog) hints.push(`File logs: ${fileLog}`);
return hints; return hints;
} }
@ -144,7 +145,10 @@ export function renderRuntimeHints(
} }
export function renderGatewayServiceStartHints(env: NodeJS.ProcessEnv = process.env): string[] { export function renderGatewayServiceStartHints(env: NodeJS.ProcessEnv = process.env): string[] {
const base = ["clawdbot daemon install", "clawdbot gateway"]; const base = [
formatCliCommand("clawdbot daemon install", env),
formatCliCommand("clawdbot gateway", env),
];
const profile = env.CLAWDBOT_PROFILE; const profile = env.CLAWDBOT_PROFILE;
switch (process.platform) { switch (process.platform) {
case "darwin": { case "darwin": {

View File

@ -13,6 +13,7 @@ import { isWSLEnv } from "../../infra/wsl.js";
import { getResolvedLoggerSettings } from "../../logging.js"; import { getResolvedLoggerSettings } from "../../logging.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { colorize, isRich, theme } from "../../terminal/theme.js"; import { colorize, isRich, theme } from "../../terminal/theme.js";
import { formatCliCommand } from "../command-format.js";
import { formatRuntimeStatus, renderRuntimeHints, safeDaemonEnv } from "./shared.js"; import { formatRuntimeStatus, renderRuntimeHints, safeDaemonEnv } from "./shared.js";
import { import {
type DaemonStatus, type DaemonStatus,
@ -70,7 +71,9 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
defaultRuntime.error(`${warnText("Service config issue:")} ${issue.message}${detail}`); defaultRuntime.error(`${warnText("Service config issue:")} ${issue.message}${detail}`);
} }
defaultRuntime.error( defaultRuntime.error(
warnText('Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").'), warnText(
`Recommendation: run "${formatCliCommand("clawdbot doctor")}" (or "${formatCliCommand("clawdbot doctor --repair")}").`,
),
); );
} }
@ -103,7 +106,7 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
); );
defaultRuntime.error( defaultRuntime.error(
errorText( errorText(
"Fix: rerun `clawdbot daemon install --force` from the same --profile / CLAWDBOT_STATE_DIR you expect.", `Fix: rerun \`${formatCliCommand("clawdbot daemon install --force")}\` from the same --profile / CLAWDBOT_STATE_DIR you expect.`,
), ),
); );
} }
@ -205,7 +208,9 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${labelValue}`, `LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${labelValue}`,
), ),
); );
defaultRuntime.error(errorText("Then reinstall: clawdbot daemon install")); defaultRuntime.error(
errorText(`Then reinstall: ${formatCliCommand("clawdbot daemon install")}`),
);
spacer(); spacer();
} }
@ -259,7 +264,7 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
for (const svc of legacyServices) { for (const svc of legacyServices) {
defaultRuntime.error(`- ${errorText(svc.label)} (${svc.detail})`); defaultRuntime.error(`- ${errorText(svc.label)} (${svc.detail})`);
} }
defaultRuntime.error(errorText("Cleanup: clawdbot doctor")); defaultRuntime.error(errorText(`Cleanup: ${formatCliCommand("clawdbot doctor")}`));
spacer(); spacer();
} }
@ -288,6 +293,6 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
spacer(); spacer();
} }
defaultRuntime.log(`${label("Troubles:")} run clawdbot status`); defaultRuntime.log(`${label("Troubles:")} run ${formatCliCommand("clawdbot status")}`);
defaultRuntime.log(`${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`); defaultRuntime.log(`${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`);
} }

View File

@ -18,6 +18,7 @@ import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js"; import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js";
import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createSubsystemLogger } from "../../logging/subsystem.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { forceFreePortAndWait } from "../ports.js"; import { forceFreePortAndWait } from "../ports.js";
import { ensureDevGatewayConfig } from "./dev.js"; import { ensureDevGatewayConfig } from "./dev.js";
import { runGatewayLoop } from "./run-loop.js"; import { runGatewayLoop } from "./run-loop.js";
@ -161,7 +162,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
if (!opts.allowUnconfigured && mode !== "local") { if (!opts.allowUnconfigured && mode !== "local") {
if (!configExists) { if (!configExists) {
defaultRuntime.error( defaultRuntime.error(
"Missing config. Run `clawdbot setup` or set gateway.mode=local (or pass --allow-unconfigured).", `Missing config. Run \`${formatCliCommand("clawdbot setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`,
); );
} else { } else {
defaultRuntime.error( defaultRuntime.error(
@ -277,7 +278,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
) { ) {
const errMessage = describeUnknownError(err); const errMessage = describeUnknownError(err);
defaultRuntime.error( defaultRuntime.error(
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot daemon stop`, `Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: ${formatCliCommand("clawdbot daemon stop")}`,
); );
try { try {
const diagnostics = await inspectPortUsage(port); const diagnostics = await inspectPortUsage(port);

View File

@ -5,6 +5,7 @@ import {
} from "../../daemon/constants.js"; } from "../../daemon/constants.js";
import { resolveGatewayService } from "../../daemon/service.js"; import { resolveGatewayService } from "../../daemon/service.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
export function parsePort(raw: unknown): number | null { export function parsePort(raw: unknown): number | null {
if (raw === undefined || raw === null) return null; if (raw === undefined || raw === null) return null;
@ -67,21 +68,21 @@ export function renderGatewayServiceStopHints(env: NodeJS.ProcessEnv = process.e
switch (process.platform) { switch (process.platform) {
case "darwin": case "darwin":
return [ return [
"Tip: clawdbot daemon stop", `Tip: ${formatCliCommand("clawdbot daemon stop")}`,
`Or: launchctl bootout gui/$UID/${resolveGatewayLaunchAgentLabel(profile)}`, `Or: launchctl bootout gui/$UID/${resolveGatewayLaunchAgentLabel(profile)}`,
]; ];
case "linux": case "linux":
return [ return [
"Tip: clawdbot daemon stop", `Tip: ${formatCliCommand("clawdbot daemon stop")}`,
`Or: systemctl --user stop ${resolveGatewaySystemdServiceName(profile)}.service`, `Or: systemctl --user stop ${resolveGatewaySystemdServiceName(profile)}.service`,
]; ];
case "win32": case "win32":
return [ return [
"Tip: clawdbot daemon stop", `Tip: ${formatCliCommand("clawdbot daemon stop")}`,
`Or: schtasks /End /TN "${resolveGatewayWindowsTaskName(profile)}"`, `Or: schtasks /End /TN "${resolveGatewayWindowsTaskName(profile)}"`,
]; ];
default: default:
return ["Tip: clawdbot daemon stop"]; return [`Tip: ${formatCliCommand("clawdbot daemon stop")}`];
} }
} }

View File

@ -24,6 +24,7 @@ import { buildPluginStatusReport } from "../plugins/status.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
export type HooksListOptions = { export type HooksListOptions = {
@ -150,7 +151,7 @@ export function formatHooksList(report: HookStatusReport, opts: HooksListOptions
if (hooks.length === 0) { if (hooks.length === 0) {
const message = opts.eligible const message = opts.eligible
? "No eligible hooks found. Run `clawdbot hooks list` to see all hooks." ? `No eligible hooks found. Run \`${formatCliCommand("clawdbot hooks list")}\` to see all hooks.`
: "No hooks found."; : "No hooks found.";
return message; return message;
} }
@ -194,7 +195,7 @@ export function formatHookInfo(
if (opts.json) { if (opts.json) {
return JSON.stringify({ error: "not found", hook: hookName }, null, 2); return JSON.stringify({ error: "not found", hook: hookName }, null, 2);
} }
return `Hook "${hookName}" not found. Run \`clawdbot hooks list\` to see available hooks.`; return `Hook "${hookName}" not found. Run \`${formatCliCommand("clawdbot hooks list")}\` to see available hooks.`;
} }
if (opts.json) { if (opts.json) {

View File

@ -5,6 +5,7 @@ import { parseLogLine } from "../logging/parse-log-line.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { colorize, isRich, theme } from "../terminal/theme.js"; import { colorize, isRich, theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js"; import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
type LogsTailPayload = { type LogsTailPayload = {
@ -117,7 +118,7 @@ function emitGatewayError(
) { ) {
const details = buildGatewayConnectionDetails({ url: opts.url }); const details = buildGatewayConnectionDetails({ url: opts.url });
const message = "Gateway not reachable. Is it running and accessible?"; const message = "Gateway not reachable. Is it running and accessible?";
const hint = "Hint: run `clawdbot doctor`."; const hint = `Hint: run \`${formatCliCommand("clawdbot doctor")}\`.`;
const errorText = err instanceof Error ? err.message : String(err); const errorText = err instanceof Error ? err.message : String(err);
if (mode === "json") { if (mode === "json") {

View File

@ -18,6 +18,7 @@ import { isWSL } from "../../infra/wsl.js";
import { loadNodeHostConfig } from "../../node-host/config.js"; import { loadNodeHostConfig } from "../../node-host/config.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { colorize, isRich, theme } from "../../terminal/theme.js"; import { colorize, isRich, theme } from "../../terminal/theme.js";
import { formatCliCommand } from "../command-format.js";
import { import {
buildDaemonServiceSnapshot, buildDaemonServiceSnapshot,
createNullWriter, createNullWriter,
@ -46,7 +47,10 @@ type NodeDaemonStatusOptions = {
}; };
function renderNodeServiceStartHints(): string[] { function renderNodeServiceStartHints(): string[] {
const base = ["clawdbot node service install", "clawdbot node start"]; const base = [
formatCliCommand("clawdbot node service install"),
formatCliCommand("clawdbot node start"),
];
switch (process.platform) { switch (process.platform) {
case "darwin": case "darwin":
return [ return [
@ -168,7 +172,9 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
}); });
if (!json) { if (!json) {
defaultRuntime.log(`Node service already ${service.loadedText}.`); defaultRuntime.log(`Node service already ${service.loadedText}.`);
defaultRuntime.log("Reinstall with: clawdbot node service install --force"); defaultRuntime.log(
`Reinstall with: ${formatCliCommand("clawdbot node service install --force")}`,
);
} }
return; return;
} }

View File

@ -10,6 +10,7 @@ import {
} from "../pairing/pairing-store.js"; } from "../pairing/pairing-store.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
/** Parse channel, allowing extension channels not in core registry. */ /** Parse channel, allowing extension channels not in core registry. */
function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel { function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel {
@ -95,12 +96,12 @@ export function registerPairingCli(program: Command) {
const resolvedCode = opts.channel ? codeOrChannel : code; const resolvedCode = opts.channel ? codeOrChannel : code;
if (!opts.channel && !code) { if (!opts.channel && !code) {
throw new Error( throw new Error(
`Usage: clawdbot pairing approve <channel> <code> (or: clawdbot pairing approve --channel <channel> <code>)`, `Usage: ${formatCliCommand("clawdbot pairing approve <channel> <code>")} (or: ${formatCliCommand("clawdbot pairing approve --channel <channel> <code>")})`,
); );
} }
if (opts.channel && code != null) { if (opts.channel && code != null) {
throw new Error( throw new Error(
`Too many arguments. Use: clawdbot pairing approve --channel <channel> <code>`, `Too many arguments. Use: ${formatCliCommand("clawdbot pairing approve --channel <channel> <code>")}`,
); );
} }
const channel = parseChannel(channelRaw, channels); const channel = parseChannel(channelRaw, channels);

15
src/cli/profile-utils.ts Normal file
View File

@ -0,0 +1,15 @@
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
export function isValidProfileName(value: string): boolean {
if (!value) return false;
// Keep it path-safe + shell-friendly.
return PROFILE_NAME_RE.test(value);
}
export function normalizeProfileName(raw?: string | null): string | null {
const profile = raw?.trim();
if (!profile) return null;
if (profile.toLowerCase() === "default") return null;
if (!isValidProfileName(profile)) return null;
return profile;
}

View File

@ -1,5 +1,6 @@
import path from "node:path"; import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { formatCliCommand } from "./command-format.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js"; import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
describe("parseCliProfileArgs", () => { describe("parseCliProfileArgs", () => {
@ -76,3 +77,63 @@ describe("applyCliProfileEnv", () => {
expect(env.CLAWDBOT_CONFIG_PATH).toBe(path.join("/custom", "clawdbot.json")); expect(env.CLAWDBOT_CONFIG_PATH).toBe(path.join("/custom", "clawdbot.json"));
}); });
}); });
describe("formatCliCommand", () => {
it("returns command unchanged when no profile is set", () => {
expect(formatCliCommand("clawdbot doctor --fix", {})).toBe("clawdbot doctor --fix");
});
it("returns command unchanged when profile is default", () => {
expect(formatCliCommand("clawdbot doctor --fix", { CLAWDBOT_PROFILE: "default" })).toBe(
"clawdbot doctor --fix",
);
});
it("returns command unchanged when profile is Default (case-insensitive)", () => {
expect(formatCliCommand("clawdbot doctor --fix", { CLAWDBOT_PROFILE: "Default" })).toBe(
"clawdbot doctor --fix",
);
});
it("returns command unchanged when profile is invalid", () => {
expect(formatCliCommand("clawdbot doctor --fix", { CLAWDBOT_PROFILE: "bad profile" })).toBe(
"clawdbot doctor --fix",
);
});
it("returns command unchanged when --profile is already present", () => {
expect(
formatCliCommand("clawdbot --profile work doctor --fix", { CLAWDBOT_PROFILE: "work" }),
).toBe("clawdbot --profile work doctor --fix");
});
it("returns command unchanged when --dev is already present", () => {
expect(formatCliCommand("clawdbot --dev doctor", { CLAWDBOT_PROFILE: "dev" })).toBe(
"clawdbot --dev doctor",
);
});
it("inserts --profile flag when profile is set", () => {
expect(formatCliCommand("clawdbot doctor --fix", { CLAWDBOT_PROFILE: "work" })).toBe(
"clawdbot --profile work doctor --fix",
);
});
it("trims whitespace from profile", () => {
expect(formatCliCommand("clawdbot doctor --fix", { CLAWDBOT_PROFILE: " jbclawd " })).toBe(
"clawdbot --profile jbclawd doctor --fix",
);
});
it("handles command with no args after clawdbot", () => {
expect(formatCliCommand("clawdbot", { CLAWDBOT_PROFILE: "test" })).toBe(
"clawdbot --profile test",
);
});
it("handles pnpm wrapper", () => {
expect(formatCliCommand("pnpm clawdbot doctor", { CLAWDBOT_PROFILE: "work" })).toBe(
"pnpm clawdbot --profile work doctor",
);
});
});

View File

@ -1,6 +1,8 @@
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { isValidProfileName } from "./profile-utils.js";
export type CliProfileParseResult = export type CliProfileParseResult =
| { ok: true; profile: string | null; argv: string[] } | { ok: true; profile: string | null; argv: string[] }
| { ok: false; error: string }; | { ok: false; error: string };
@ -21,12 +23,6 @@ function takeValue(
return { value: trimmed || null, consumedNext: Boolean(next) }; return { value: trimmed || null, consumedNext: Boolean(next) };
} }
function isValidProfileName(value: string): boolean {
if (!value) return false;
// Keep it path-safe + shell-friendly.
return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(value);
}
export function parseCliProfileArgs(argv: string[]): CliProfileParseResult { export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
if (argv.length < 2) return { ok: true, profile: null, argv }; if (argv.length < 2) return { ok: true, profile: null, argv };

View File

@ -4,6 +4,7 @@ import { loadAndMaybeMigrateDoctorConfig } from "../../commands/doctor-config-fl
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { loadClawdbotPlugins } from "../../plugins/loader.js"; import { loadClawdbotPlugins } from "../../plugins/loader.js";
import type { RuntimeEnv } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
const ALLOWED_INVALID_COMMANDS = new Set(["doctor", "logs", "health", "help", "status", "service"]); const ALLOWED_INVALID_COMMANDS = new Set(["doctor", "logs", "health", "help", "status", "service"]);
@ -72,7 +73,9 @@ export async function ensureConfigReady(params: {
params.runtime.error(pluginIssues.map((issue) => ` ${error(issue)}`).join("\n")); params.runtime.error(pluginIssues.map((issue) => ` ${error(issue)}`).join("\n"));
} }
params.runtime.error(""); params.runtime.error("");
params.runtime.error(`${muted("Run:")} ${commandText("clawdbot doctor --fix")}`); params.runtime.error(
`${muted("Run:")} ${commandText(formatCliCommand("clawdbot doctor --fix"))}`,
);
if (!allowInvalid) { if (!allowInvalid) {
params.runtime.exit(1); params.runtime.exit(1);
} }

View File

@ -7,6 +7,7 @@ import { runSecurityAudit } from "../security/audit.js";
import { fixSecurityFootguns } from "../security/fix.js"; import { fixSecurityFootguns } from "../security/fix.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { isRich, theme } from "../terminal/theme.js"; import { isRich, theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
type SecurityAuditOptions = { type SecurityAuditOptions = {
json?: boolean; json?: boolean;
@ -67,10 +68,10 @@ export function registerSecurityCli(program: Command) {
const lines: string[] = []; const lines: string[] = [];
lines.push(heading("Clawdbot security audit")); lines.push(heading("Clawdbot security audit"));
lines.push(muted(`Summary: ${formatSummary(report.summary)}`)); lines.push(muted(`Summary: ${formatSummary(report.summary)}`));
lines.push(muted(`Run deeper: clawdbot security audit --deep`)); lines.push(muted(`Run deeper: ${formatCliCommand("clawdbot security audit --deep")}`));
if (opts.fix) { if (opts.fix) {
lines.push(muted(`Fix: clawdbot security audit --fix`)); lines.push(muted(`Fix: ${formatCliCommand("clawdbot security audit --fix")}`));
if (!fixResult) { if (!fixResult) {
lines.push(muted("Fixes: failed to apply (unexpected error)")); lines.push(muted("Fixes: failed to apply (unexpected error)"));
} else if ( } else if (

View File

@ -10,6 +10,7 @@ import { loadConfig } from "../config/config.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
export type SkillsListOptions = { export type SkillsListOptions = {
json?: boolean; json?: boolean;
@ -101,7 +102,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
if (skills.length === 0) { if (skills.length === 0) {
const message = opts.eligible const message = opts.eligible
? "No eligible skills found. Run `clawdbot skills list` to see all skills." ? `No eligible skills found. Run \`${formatCliCommand("clawdbot skills list")}\` to see all skills.`
: "No skills found."; : "No skills found.";
return appendClawdHubHint(message, opts.json); return appendClawdHubHint(message, opts.json);
} }
@ -148,7 +149,7 @@ export function formatSkillInfo(
return JSON.stringify({ error: "not found", skill: skillName }, null, 2); return JSON.stringify({ error: "not found", skill: skillName }, null, 2);
} }
return appendClawdHubHint( return appendClawdHubHint(
`Skill "${skillName}" not found. Run \`clawdbot skills list\` to see available skills.`, `Skill "${skillName}" not found. Run \`${formatCliCommand("clawdbot skills list")}\` to see available skills.`,
opts.json, opts.json,
); );
} }

View File

@ -15,6 +15,7 @@ import {
} from "../infra/update-runner.js"; } from "../infra/update-runner.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { formatCliCommand } from "./command-format.js";
import { stylePromptMessage } from "../terminal/prompt-style.js"; import { stylePromptMessage } from "../terminal/prompt-style.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
@ -373,7 +374,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
if (result.reason === "not-git-install") { if (result.reason === "not-git-install") {
defaultRuntime.log( defaultRuntime.log(
theme.warn( theme.warn(
"Skipped: this Clawdbot install isn't a git checkout, and the package manager couldn't be detected. Update via your package manager, then run `clawdbot doctor` and `clawdbot daemon restart`.", `Skipped: this Clawdbot install isn't a git checkout, and the package manager couldn't be detected. Update via your package manager, then run \`${formatCliCommand("clawdbot doctor")}\` and \`${formatCliCommand("clawdbot daemon restart")}\`.`,
), ),
); );
defaultRuntime.log( defaultRuntime.log(
@ -410,7 +411,9 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
if (!opts.json) { if (!opts.json) {
defaultRuntime.log(theme.warn(`Daemon restart failed: ${String(err)}`)); defaultRuntime.log(theme.warn(`Daemon restart failed: ${String(err)}`));
defaultRuntime.log( defaultRuntime.log(
theme.muted("You may need to restart the daemon manually: clawdbot daemon restart"), theme.muted(
`You may need to restart the daemon manually: ${formatCliCommand("clawdbot daemon restart")}`,
),
); );
} }
} }
@ -419,12 +422,14 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
if (result.mode === "npm" || result.mode === "pnpm") { if (result.mode === "npm" || result.mode === "pnpm") {
defaultRuntime.log( defaultRuntime.log(
theme.muted( theme.muted(
"Tip: Run `clawdbot doctor`, then `clawdbot daemon restart` to apply updates to a running gateway.", `Tip: Run \`${formatCliCommand("clawdbot doctor")}\`, then \`${formatCliCommand("clawdbot daemon restart")}\` to apply updates to a running gateway.`,
), ),
); );
} else { } else {
defaultRuntime.log( defaultRuntime.log(
theme.muted("Tip: Run `clawdbot daemon restart` to apply updates to a running gateway."), theme.muted(
`Tip: Run \`${formatCliCommand("clawdbot daemon restart")}\` to apply updates to a running gateway.`,
),
); );
} }
} }

View File

@ -7,6 +7,7 @@ import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
import { listAgentIds } from "../agents/agent-scope.js"; import { listAgentIds } from "../agents/agent-scope.js";
import { normalizeAgentId } from "../routing/session-key.js"; import { normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { import {
GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES, GATEWAY_CLIENT_NAMES,
@ -92,7 +93,7 @@ export async function agentViaGatewayCommand(opts: AgentCliOpts, runtime: Runtim
const knownAgents = listAgentIds(cfg); const knownAgents = listAgentIds(cfg);
if (!knownAgents.includes(agentId)) { if (!knownAgents.includes(agentId)) {
throw new Error( throw new Error(
`Unknown agent id "${agentIdRaw}". Use "clawdbot agents list" to see configured agents.`, `Unknown agent id "${agentIdRaw}". Use "${formatCliCommand("clawdbot agents list")}" to see configured agents.`,
); );
} }
} }

View File

@ -47,6 +47,7 @@ import {
} from "../infra/agent-events.js"; } from "../infra/agent-events.js";
import { getRemoteSkillEligibility } from "../infra/skills-remote.js"; import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { applyVerboseOverride } from "../sessions/level-overrides.js"; import { applyVerboseOverride } from "../sessions/level-overrides.js";
import { resolveSendPolicy } from "../sessions/send-policy.js"; import { resolveSendPolicy } from "../sessions/send-policy.js";
import { resolveMessageChannel } from "../utils/message-channel.js"; import { resolveMessageChannel } from "../utils/message-channel.js";
@ -75,7 +76,7 @@ export async function agentCommand(
const knownAgents = listAgentIds(cfg); const knownAgents = listAgentIds(cfg);
if (!knownAgents.includes(agentIdOverride)) { if (!knownAgents.includes(agentIdOverride)) {
throw new Error( throw new Error(
`Unknown agent id "${agentIdOverrideRaw}". Use "clawdbot agents list" to see configured agents.`, `Unknown agent id "${agentIdOverrideRaw}". Use "${formatCliCommand("clawdbot agents list")}" to see configured agents.`,
); );
} }
} }

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../cli/command-format.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { readConfigFileSnapshot } from "../config/config.js"; import { readConfigFileSnapshot } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
@ -14,7 +15,7 @@ export async function requireValidConfig(runtime: RuntimeEnv): Promise<ClawdbotC
? snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n") ? snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n")
: "Unknown validation issue."; : "Unknown validation issue.";
runtime.error(`Config invalid:\n${issues}`); runtime.error(`Config invalid:\n${issues}`);
runtime.error("Fix the config or run clawdbot doctor."); runtime.error(`Fix the config or run ${formatCliCommand("clawdbot doctor")}.`);
runtime.exit(1); runtime.exit(1);
return null; return null;
} }

View File

@ -2,6 +2,7 @@ import type { AgentBinding } from "../config/types.js";
import { normalizeAgentId } from "../routing/session-key.js"; import { normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { describeBinding } from "./agents.bindings.js"; import { describeBinding } from "./agents.bindings.js";
import { requireValidConfig } from "./agents.command-shared.js"; import { requireValidConfig } from "./agents.command-shared.js";
import type { AgentSummary } from "./agents.config.js"; import type { AgentSummary } from "./agents.config.js";
@ -116,7 +117,7 @@ export async function agentsListCommand(
const lines = ["Agents:", ...summaries.map(formatSummary)]; const lines = ["Agents:", ...summaries.map(formatSummary)];
lines.push("Routing rules map channel/account/peer to an agent. Use --bindings for full rules."); lines.push("Routing rules map channel/account/peer to an agent. Use --bindings for full rules.");
lines.push( lines.push(
"Channel status reflects local config/creds. For live health: clawdbot channels status --probe.", `Channel status reflects local config/creds. For live health: ${formatCliCommand("clawdbot channels status --probe")}.`,
); );
runtime.log(lines.join("\n")); runtime.log(lines.join("\n"));
} }

View File

@ -1,4 +1,5 @@
import { type ChannelId, getChannelPlugin } from "../../channels/plugins/index.js"; import { type ChannelId, getChannelPlugin } from "../../channels/plugins/index.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { type ClawdbotConfig, readConfigFileSnapshot } from "../../config/config.js"; import { type ClawdbotConfig, readConfigFileSnapshot } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
@ -15,7 +16,7 @@ export async function requireValidConfig(
? snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n") ? snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n")
: "Unknown validation issue."; : "Unknown validation issue.";
runtime.error(`Config invalid:\n${issues}`); runtime.error(`Config invalid:\n${issues}`);
runtime.error("Fix the config or run clawdbot doctor."); runtime.error(`Fix the config or run ${formatCliCommand("clawdbot doctor")}.`);
runtime.exit(1); runtime.exit(1);
return null; return null;
} }

View File

@ -8,6 +8,7 @@ import { formatAge } from "../../infra/channel-summary.js";
import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js"; import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js"; import { formatDocsLink } from "../../terminal/links.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { theme } from "../../terminal/theme.js"; import { theme } from "../../terminal/theme.js";
import { type ChatChannel, formatChannelAccountLabel, requireValidConfig } from "./shared.js"; import { type ChatChannel, formatChannelAccountLabel, requireValidConfig } from "./shared.js";
@ -142,7 +143,7 @@ export function formatGatewayChannelsStatusLines(payload: Record<string, unknown
`- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`, `- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
); );
} }
lines.push(`- Run: clawdbot doctor`); lines.push(`- Run: ${formatCliCommand("clawdbot doctor")}`);
lines.push(""); lines.push("");
} }
lines.push( lines.push(

View File

@ -1,4 +1,5 @@
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js"; import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { CONFIG_PATH_CLAWDBOT } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
@ -23,7 +24,7 @@ export async function removeChannelConfigWizard(
note( note(
[ [
"No channel config found in clawdbot.json.", "No channel config found in clawdbot.json.",
"Tip: `clawdbot channels status` shows what is configured and enabled.", `Tip: \`${formatCliCommand("clawdbot channels status")}\` shows what is configured and enabled.`,
].join("\n"), ].join("\n"),
"Remove channel", "Remove channel",
); );

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../cli/command-format.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { import {
CONFIG_PATH_CLAWDBOT, CONFIG_PATH_CLAWDBOT,
@ -198,7 +199,9 @@ export async function runConfigureWizard(
); );
} }
if (!snapshot.valid) { if (!snapshot.valid) {
outro("Config invalid. Run `clawdbot doctor` to repair it, then re-run configure."); outro(
`Config invalid. Run \`${formatCliCommand("clawdbot doctor")}\` to repair it, then re-run configure.`,
);
runtime.exit(1); runtime.exit(1);
return; return;
} }

View File

@ -6,6 +6,7 @@ import {
resolveSystemNodeInfo, resolveSystemNodeInfo,
} from "../daemon/runtime-paths.js"; } from "../daemon/runtime-paths.js";
import { buildServiceEnvironment } from "../daemon/service-env.js"; import { buildServiceEnvironment } from "../daemon/service-env.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { GatewayDaemonRuntime } from "./daemon-runtime.js"; import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
type WarnFn = (message: string, title?: string) => void; type WarnFn = (message: string, title?: string) => void;
@ -65,5 +66,5 @@ export async function buildGatewayInstallPlan(params: {
export function gatewayInstallErrorHint(platform = process.platform): string { export function gatewayInstallErrorHint(platform = process.platform): string {
return platform === "win32" return platform === "win32"
? "Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install." ? "Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install."
: "Tip: rerun `clawdbot daemon install` after fixing the error."; : `Tip: rerun \`${formatCliCommand("clawdbot daemon install")}\` after fixing the error.`;
} }

View File

@ -3,6 +3,7 @@ import { runCommandWithTimeout } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { isRich, theme } from "../terminal/theme.js"; import { isRich, theme } from "../terminal/theme.js";
import { formatCliCommand } from "../cli/command-format.js";
const SEARCH_TOOL = "https://docs.clawd.bot/mcp.SearchClawdbot"; const SEARCH_TOOL = "https://docs.clawd.bot/mcp.SearchClawdbot";
const SEARCH_TIMEOUT_MS = 30_000; const SEARCH_TIMEOUT_MS = 30_000;
@ -150,10 +151,10 @@ export async function docsSearchCommand(queryParts: string[], runtime: RuntimeEn
const docs = formatDocsLink("/", "docs.clawd.bot"); const docs = formatDocsLink("/", "docs.clawd.bot");
if (isRich()) { if (isRich()) {
runtime.log(`${theme.muted("Docs:")} ${docs}`); runtime.log(`${theme.muted("Docs:")} ${docs}`);
runtime.log(`${theme.muted("Search:")} clawdbot docs "your query"`); runtime.log(`${theme.muted("Search:")} ${formatCliCommand('clawdbot docs "your query"')}`);
} else { } else {
runtime.log("Docs: https://docs.clawd.bot/"); runtime.log("Docs: https://docs.clawd.bot/");
runtime.log('Search: clawdbot docs "your query"'); runtime.log(`Search: ${formatCliCommand('clawdbot docs "your query"')}`);
} }
return; return;
} }

View File

@ -13,6 +13,7 @@ import {
} from "../agents/auth-profiles.js"; } from "../agents/auth-profiles.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { DoctorPrompter } from "./doctor-prompter.js"; import type { DoctorPrompter } from "./doctor-prompter.js";
export async function maybeRepairAnthropicOAuthProfileId( export async function maybeRepairAnthropicOAuthProfileId(
@ -49,9 +50,9 @@ function formatAuthIssueHint(issue: AuthIssue): string | null {
return "Run `claude setup-token` on the gateway host."; return "Run `claude setup-token` on the gateway host.";
} }
if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) { if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) {
return "Run `codex login` (or `clawdbot configure` → OpenAI Codex OAuth)."; return `Run \`codex login\` (or \`${formatCliCommand("clawdbot configure")}\` → OpenAI Codex OAuth).`;
} }
return "Re-auth via `clawdbot configure` or `clawdbot onboard`."; return `Re-auth via \`${formatCliCommand("clawdbot configure")}\` or \`${formatCliCommand("clawdbot onboard")}\`.`;
} }
function formatAuthIssueLine(issue: AuthIssue): string { function formatAuthIssueLine(issue: AuthIssue): string {

View File

@ -8,6 +8,7 @@ import {
readConfigFileSnapshot, readConfigFileSnapshot,
} from "../config/config.js"; } from "../config/config.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { formatCliCommand } from "../cli/command-format.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js"; import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
import type { DoctorOptions } from "./doctor-prompter.js"; import type { DoctorOptions } from "./doctor-prompter.js";
@ -139,7 +140,10 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
if (changes.length > 0) note(changes.join("\n"), "Doctor changes"); if (changes.length > 0) note(changes.join("\n"), "Doctor changes");
if (migrated) cfg = migrated; if (migrated) cfg = migrated;
} else { } else {
note('Run "clawdbot doctor --fix" to apply legacy migrations.', "Doctor"); note(
`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply legacy migrations.`,
"Doctor",
);
} }
} }
@ -149,7 +153,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
if (shouldRepair) { if (shouldRepair) {
cfg = normalized.config; cfg = normalized.config;
} else { } else {
note('Run "clawdbot doctor --fix" to apply these changes.', "Doctor"); note(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply these changes.`, "Doctor");
} }
} }
@ -159,7 +163,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
if (shouldRepair) { if (shouldRepair) {
cfg = autoEnable.config; cfg = autoEnable.config;
} else { } else {
note('Run "clawdbot doctor --fix" to apply these changes.', "Doctor"); note(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply these changes.`, "Doctor");
} }
} }

View File

@ -8,6 +8,7 @@ import {
isSystemdUnavailableDetail, isSystemdUnavailableDetail,
renderSystemdUnavailableHints, renderSystemdUnavailableHints,
} from "../daemon/systemd-hints.js"; } from "../daemon/systemd-hints.js";
import { formatCliCommand } from "../cli/command-format.js";
import { isWSLEnv } from "../infra/wsl.js"; import { isWSLEnv } from "../infra/wsl.js";
import type { GatewayServiceRuntime } from "../daemon/service-runtime.js"; import type { GatewayServiceRuntime } from "../daemon/service-runtime.js";
import { getResolvedLoggerSettings } from "../logging.js"; import { getResolvedLoggerSettings } from "../logging.js";
@ -69,10 +70,10 @@ export function buildGatewayRuntimeHints(
hints.push( hints.push(
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${label}`, `LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${label}`,
); );
hints.push("Then reinstall: clawdbot daemon install"); hints.push(`Then reinstall: ${formatCliCommand("clawdbot daemon install", env)}`);
} }
if (runtime.missingUnit) { if (runtime.missingUnit) {
hints.push("Service not installed. Run: clawdbot daemon install"); hints.push(`Service not installed. Run: ${formatCliCommand("clawdbot daemon install", env)}`);
if (fileLog) hints.push(`File logs: ${fileLog}`); if (fileLog) hints.push(`File logs: ${fileLog}`);
return hints; return hints;
} }

View File

@ -17,6 +17,7 @@ import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js";
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js"; import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
import { isWSL } from "../infra/wsl.js"; import { isWSL } from "../infra/wsl.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { sleep } from "../utils.js"; import { sleep } from "../utils.js";
import { import {
@ -201,7 +202,7 @@ export async function maybeRepairGatewayDaemon(params: {
if (process.platform === "darwin") { if (process.platform === "darwin") {
const label = resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE); const label = resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE);
note( note(
`LaunchAgent loaded; stopping requires "clawdbot daemon stop" or launchctl bootout gui/$UID/${label}.`, `LaunchAgent loaded; stopping requires "${formatCliCommand("clawdbot daemon stop")}" or launchctl bootout gui/$UID/${label}.`,
"Gateway", "Gateway",
); );
} }

View File

@ -4,10 +4,11 @@ import type { ChannelId } from "../channels/plugins/types.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { formatCliCommand } from "../cli/command-format.js";
export async function noteSecurityWarnings(cfg: ClawdbotConfig) { export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
const warnings: string[] = []; const warnings: string[] = [];
const auditHint = `- Run: clawdbot security audit --deep`; const auditHint = `- Run: ${formatCliCommand("clawdbot security audit --deep")}`;
const warnDmPolicy = async (params: { const warnDmPolicy = async (params: {
label: string; label: string;

View File

@ -3,6 +3,7 @@ import { isTruthyEnvValue } from "../infra/env.js";
import { runCommandWithTimeout } from "../process/exec.js"; import { runCommandWithTimeout } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { DoctorOptions } from "./doctor-prompter.js"; import type { DoctorOptions } from "./doctor-prompter.js";
async function detectClawdbotGitCheckout(root: string): Promise<"git" | "not-git" | "unknown"> { async function detectClawdbotGitCheckout(root: string): Promise<"git" | "not-git" | "unknown"> {
@ -71,7 +72,7 @@ export async function maybeOfferUpdateBeforeDoctor(params: {
note( note(
[ [
"This install is not a git checkout.", "This install is not a git checkout.",
"Run `clawdbot update` to update via your package manager (npm/pnpm), then rerun doctor.", `Run \`${formatCliCommand("clawdbot update")}\` to update via your package manager (npm/pnpm), then rerun doctor.`,
].join("\n"), ].join("\n"),
"Update", "Update",
); );

View File

@ -9,6 +9,7 @@ import {
resolveConfiguredModelRef, resolveConfiguredModelRef,
resolveHooksGmailModel, resolveHooksGmailModel,
} from "../agents/model-selection.js"; } from "../agents/model-selection.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, writeConfigFile } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
import { resolveGatewayService } from "../daemon/service.js"; import { resolveGatewayService } from "../daemon/service.js";
@ -258,7 +259,7 @@ export async function doctorCommand(
runtime.log(`Backup: ${backupPath}`); runtime.log(`Backup: ${backupPath}`);
} }
} else { } else {
runtime.log('Run "clawdbot doctor --fix" to apply changes.'); runtime.log(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply changes.`);
} }
if (options.workspaceSuggestions !== false) { if (options.workspaceSuggestions !== false) {

View File

@ -15,6 +15,7 @@ import {
} from "../../agents/agent-scope.js"; } from "../../agents/agent-scope.js";
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import { parseDurationMs } from "../../cli/parse-duration.js"; import { parseDurationMs } from "../../cli/parse-duration.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { import {
CONFIG_PATH_CLAWDBOT, CONFIG_PATH_CLAWDBOT,
readConfigFileSnapshot, readConfigFileSnapshot,
@ -340,7 +341,9 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
const providers = resolvePluginProviders({ config, workspaceDir }); const providers = resolvePluginProviders({ config, workspaceDir });
if (providers.length === 0) { if (providers.length === 0) {
throw new Error("No provider plugins found. Install one via `clawdbot plugins install`."); throw new Error(
`No provider plugins found. Install one via \`${formatCliCommand("clawdbot plugins install")}\`.`,
);
} }
const prompter = createClackPrompter(); const prompter = createClackPrompter();

View File

@ -22,6 +22,7 @@ import {
} from "../../infra/provider-usage.js"; } from "../../infra/provider-usage.js";
import type { RuntimeEnv } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js";
import { colorize, theme } from "../../terminal/theme.js"; import { colorize, theme } from "../../terminal/theme.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { shortenHomePath } from "../../utils.js"; import { shortenHomePath } from "../../utils.js";
import { resolveProviderAuthOverview } from "./list.auth-overview.js"; import { resolveProviderAuthOverview } from "./list.auth-overview.js";
import { isRich } from "./list.format.js"; import { isRich } from "./list.format.js";
@ -395,8 +396,8 @@ export async function modelsStatusCommand(
for (const provider of missingProvidersInUse) { for (const provider of missingProvidersInUse) {
const hint = const hint =
provider === "anthropic" provider === "anthropic"
? "Run `claude setup-token` or `clawdbot configure`." ? `Run \`claude setup-token\` or \`${formatCliCommand("clawdbot configure")}\`.`
: "Run `clawdbot configure` or set an API key env var."; : `Run \`${formatCliCommand("clawdbot configure")}\` or set an API key env var.`;
runtime.log(`- ${theme.heading(provider)} ${hint}`); runtime.log(`- ${theme.heading(provider)} ${hint}`);
} }
} }

View File

@ -14,6 +14,7 @@ import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { formatCliCommand } from "../cli/command-format.js";
import { enablePluginInConfig } from "../plugins/enable.js"; import { enablePluginInConfig } from "../plugins/enable.js";
import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js"; import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js";
import type { ChannelChoice } from "./onboard-types.js"; import type { ChannelChoice } from "./onboard-types.js";
@ -186,7 +187,7 @@ async function noteChannelPrimer(
await prompter.note( await prompter.note(
[ [
"DM security: default is pairing; unknown DMs get a pairing code.", "DM security: default is pairing; unknown DMs get a pairing code.",
"Approve with: clawdbot pairing approve <channel> <code>", `Approve with: ${formatCliCommand("clawdbot pairing approve <channel> <code>")}`,
'Public DMs require dmPolicy="open" + allowFrom=["*"].', 'Public DMs require dmPolicy="open" + allowFrom=["*"].',
'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.', 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.',
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`, `Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
@ -233,7 +234,7 @@ async function maybeConfigureDmPolicies(params: {
await prompter.note( await prompter.note(
[ [
"Default: pairing (unknown DMs get a pairing code).", "Default: pairing (unknown DMs get a pairing code).",
`Approve: clawdbot pairing approve ${policy.channel} <code>`, `Approve: ${formatCliCommand(`clawdbot pairing approve ${policy.channel} <code>`)}`,
`Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`, `Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`,
`Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`, `Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.', 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.',
@ -581,7 +582,7 @@ export async function setupChannels(
{ {
value: "__skip__", value: "__skip__",
label: "Skip for now", label: "Skip for now",
hint: "You can add channels later via `clawdbot channels add`", hint: `You can add channels later via \`${formatCliCommand("clawdbot channels add")}\``,
}, },
], ],
initialValue: quickstartDefault, initialValue: quickstartDefault,

View File

@ -3,6 +3,7 @@ import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js"; import type { WizardPrompter } from "../wizard/prompts.js";
import { buildWorkspaceHookStatus } from "../hooks/hooks-status.js"; import { buildWorkspaceHookStatus } from "../hooks/hooks-status.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { formatCliCommand } from "../cli/command-format.js";
export async function setupInternalHooks( export async function setupInternalHooks(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
@ -73,9 +74,9 @@ export async function setupInternalHooks(
`Enabled ${selected.length} hook${selected.length > 1 ? "s" : ""}: ${selected.join(", ")}`, `Enabled ${selected.length} hook${selected.length > 1 ? "s" : ""}: ${selected.join(", ")}`,
"", "",
"You can manage hooks later with:", "You can manage hooks later with:",
" clawdbot hooks list", ` ${formatCliCommand("clawdbot hooks list")}`,
" clawdbot hooks enable <name>", ` ${formatCliCommand("clawdbot hooks enable <name>")}`,
" clawdbot hooks disable <name>", ` ${formatCliCommand("clawdbot hooks disable <name>")}`,
].join("\n"), ].join("\n"),
"Hooks Configured", "Hooks Configured",
); );

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../cli/command-format.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { readConfigFileSnapshot } from "../config/config.js"; import { readConfigFileSnapshot } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
@ -12,7 +13,9 @@ export async function runNonInteractiveOnboarding(
) { ) {
const snapshot = await readConfigFileSnapshot(); const snapshot = await readConfigFileSnapshot();
if (snapshot.exists && !snapshot.valid) { if (snapshot.exists && !snapshot.valid) {
runtime.error("Config invalid. Run `clawdbot doctor` to repair it, then re-run onboarding."); runtime.error(
`Config invalid. Run \`${formatCliCommand("clawdbot doctor")}\` to repair it, then re-run onboarding.`,
);
runtime.exit(1); runtime.exit(1);
return; return;
} }

View File

@ -1,6 +1,7 @@
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { CONFIG_PATH_CLAWDBOT, resolveGatewayPort, writeConfigFile } from "../../config/config.js"; import { CONFIG_PATH_CLAWDBOT, resolveGatewayPort, writeConfigFile } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { DEFAULT_GATEWAY_DAEMON_RUNTIME } from "../daemon-runtime.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME } from "../daemon-runtime.js";
import { healthCommand } from "../health.js"; import { healthCommand } from "../health.js";
import { import {
@ -123,7 +124,7 @@ export async function runNonInteractiveOnboardingLocal(params: {
if (!opts.json) { if (!opts.json) {
runtime.log( runtime.log(
"Tip: run `clawdbot configure --section web` to store your Brave API key for web_search. Docs: https://docs.clawd.bot/tools/web", `Tip: run \`${formatCliCommand("clawdbot configure --section web")}\` to store your Brave API key for web_search. Docs: https://docs.clawd.bot/tools/web`,
); );
} }
} }

View File

@ -1,6 +1,7 @@
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../../config/config.js"; import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { applyWizardMetadata } from "../onboard-helpers.js"; import { applyWizardMetadata } from "../onboard-helpers.js";
import type { OnboardOptions } from "../onboard-types.js"; import type { OnboardOptions } from "../onboard-types.js";
@ -45,7 +46,7 @@ export async function runNonInteractiveOnboardingRemote(params: {
runtime.log(`Remote gateway: ${remoteUrl}`); runtime.log(`Remote gateway: ${remoteUrl}`);
runtime.log(`Auth: ${payload.auth}`); runtime.log(`Auth: ${payload.auth}`);
runtime.log( runtime.log(
"Tip: run `clawdbot configure --section web` to store your Brave API key for web_search. Docs: https://docs.clawd.bot/tools/web", `Tip: run \`${formatCliCommand("clawdbot configure --section web")}\` to store your Brave API key for web_search. Docs: https://docs.clawd.bot/tools/web`,
); );
} }
} }

View File

@ -1,5 +1,6 @@
import { installSkill } from "../agents/skills-install.js"; import { installSkill } from "../agents/skills-install.js";
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js"; import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js"; import type { WizardPrompter } from "../wizard/prompts.js";
@ -152,7 +153,9 @@ export async function setupSkills(
spin.stop(`Install failed: ${name}${code}${detail ? `${detail}` : ""}`); spin.stop(`Install failed: ${name}${code}${detail ? `${detail}` : ""}`);
if (result.stderr) runtime.log(result.stderr.trim()); if (result.stderr) runtime.log(result.stderr.trim());
else if (result.stdout) runtime.log(result.stdout.trim()); else if (result.stdout) runtime.log(result.stdout.trim());
runtime.log("Tip: run `clawdbot doctor` to review skills + requirements."); runtime.log(
`Tip: run \`${formatCliCommand("clawdbot doctor")}\` to review skills + requirements.`,
);
runtime.log("Docs: https://docs.clawd.bot/skills"); runtime.log("Docs: https://docs.clawd.bot/skills");
} }
} }

View File

@ -6,6 +6,7 @@ import { resolveUserPath } from "../utils.js";
import { DEFAULT_WORKSPACE, handleReset } from "./onboard-helpers.js"; import { DEFAULT_WORKSPACE, handleReset } from "./onboard-helpers.js";
import { runInteractiveOnboarding } from "./onboard-interactive.js"; import { runInteractiveOnboarding } from "./onboard-interactive.js";
import { runNonInteractiveOnboarding } from "./onboard-non-interactive.js"; import { runNonInteractiveOnboarding } from "./onboard-non-interactive.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OnboardOptions } from "./onboard-types.js"; import type { OnboardOptions } from "./onboard-types.js";
export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) { export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) {
@ -18,7 +19,7 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
[ [
"Non-interactive onboarding requires explicit risk acknowledgement.", "Non-interactive onboarding requires explicit risk acknowledgement.",
"Read: https://docs.clawd.bot/security", "Read: https://docs.clawd.bot/security",
"Re-run with: clawdbot onboard --non-interactive --accept-risk ...", `Re-run with: ${formatCliCommand("clawdbot onboard --non-interactive --accept-risk ...")}`,
].join("\n"), ].join("\n"),
); );
runtime.exit(1); runtime.exit(1);

View File

@ -10,6 +10,7 @@ import {
import { resolveGatewayService } from "../daemon/service.js"; import { resolveGatewayService } from "../daemon/service.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js"; import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
import { formatCliCommand } from "../cli/command-format.js";
import { import {
collectWorkspaceDirs, collectWorkspaceDirs,
isPathWithin, isPathWithin,
@ -143,7 +144,7 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) {
for (const dir of sessionDirs) { for (const dir of sessionDirs) {
await removePath(dir, runtime, { dryRun, label: dir }); await removePath(dir, runtime, { dryRun, label: dir });
} }
runtime.log("Next: clawdbot onboard --install-daemon"); runtime.log(`Next: ${formatCliCommand("clawdbot onboard --install-daemon")}`);
return; return;
} }
@ -158,7 +159,7 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) {
for (const workspace of workspaceDirs) { for (const workspace of workspaceDirs) {
await removePath(workspace, runtime, { dryRun, label: workspace }); await removePath(workspace, runtime, { dryRun, label: workspace });
} }
runtime.log("Next: clawdbot onboard --install-daemon"); runtime.log(`Next: ${formatCliCommand("clawdbot onboard --install-daemon")}`);
return; return;
} }
} }

View File

@ -3,6 +3,7 @@
*/ */
import type { SandboxBrowserInfo, SandboxContainerInfo } from "../agents/sandbox.js"; import type { SandboxBrowserInfo, SandboxContainerInfo } from "../agents/sandbox.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { import {
formatAge, formatAge,
@ -88,7 +89,9 @@ export function displaySummary(
if (mismatchCount > 0) { if (mismatchCount > 0) {
runtime.log(`\n⚠ ${mismatchCount} container(s) with image mismatch detected.`); runtime.log(`\n⚠ ${mismatchCount} container(s) with image mismatch detected.`);
runtime.log(` Run 'clawdbot sandbox recreate --all' to update all containers.`); runtime.log(
` Run '${formatCliCommand("clawdbot sandbox recreate --all")}' to update all containers.`,
);
} }
} }

View File

@ -1,5 +1,6 @@
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js"; import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
import { withProgress } from "../cli/progress.js"; import { withProgress } from "../cli/progress.js";
import { formatCliCommand } from "../cli/command-format.js";
import { loadConfig, readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js"; import { loadConfig, readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js";
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js"; import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
import type { GatewayService } from "../daemon/service.js"; import type { GatewayService } from "../daemon/service.js";
@ -337,7 +338,7 @@ export async function statusAllCommand(
Item: "Gateway", Item: "Gateway",
Value: `${gatewayMode}${remoteUrlMissing ? " (remote.url missing)" : ""} · ${gatewayTarget} (${connection.urlSource}) · ${gatewayStatus}${gatewayAuth}`, Value: `${gatewayMode}${remoteUrlMissing ? " (remote.url missing)" : ""} · ${gatewayTarget} (${connection.urlSource}) · ${gatewayStatus}${gatewayAuth}`,
}, },
{ Item: "Security", Value: "Run: clawdbot security audit --deep" }, { Item: "Security", Value: `Run: ${formatCliCommand("clawdbot security audit --deep")}` },
gatewaySelfLine gatewaySelfLine
? { Item: "Gateway self", Value: gatewaySelfLine } ? { Item: "Gateway self", Value: gatewaySelfLine }
: { Item: "Gateway self", Value: "unknown" }, : { Item: "Gateway self", Value: "unknown" },

View File

@ -7,6 +7,7 @@ import type { RuntimeEnv } from "../runtime.js";
import { runSecurityAudit } from "../security/audit.js"; import { runSecurityAudit } from "../security/audit.js";
import { renderTable } from "../terminal/table.js"; import { renderTable } from "../terminal/table.js";
import { theme } from "../terminal/theme.js"; import { theme } from "../terminal/theme.js";
import { formatCliCommand } from "../cli/command-format.js";
import { import {
resolveMemoryCacheSummary, resolveMemoryCacheSummary,
resolveMemoryFtsState, resolveMemoryFtsState,
@ -374,8 +375,8 @@ export async function statusCommand(
runtime.log(theme.muted(`… +${sorted.length - shown.length} more`)); runtime.log(theme.muted(`… +${sorted.length - shown.length} more`));
} }
} }
runtime.log(theme.muted("Full report: clawdbot security audit")); runtime.log(theme.muted(`Full report: ${formatCliCommand("clawdbot security audit")}`));
runtime.log(theme.muted("Deep probe: clawdbot security audit --deep")); runtime.log(theme.muted(`Deep probe: ${formatCliCommand("clawdbot security audit --deep")}`));
runtime.log(""); runtime.log("");
runtime.log(theme.heading("Channels")); runtime.log(theme.heading("Channels"));
@ -531,11 +532,11 @@ export async function statusCommand(
runtime.log(""); runtime.log("");
} }
runtime.log("Next steps:"); runtime.log("Next steps:");
runtime.log(" Need to share? clawdbot status --all"); runtime.log(` Need to share? ${formatCliCommand("clawdbot status --all")}`);
runtime.log(" Need to debug live? clawdbot logs --follow"); runtime.log(` Need to debug live? ${formatCliCommand("clawdbot logs --follow")}`);
if (gatewayReachable) { if (gatewayReachable) {
runtime.log(" Need to test channels? clawdbot status --deep"); runtime.log(` Need to test channels? ${formatCliCommand("clawdbot status --deep")}`);
} else { } else {
runtime.log(" Fix reachability first: clawdbot gateway status"); runtime.log(` Fix reachability first: ${formatCliCommand("clawdbot gateway status")}`);
} }
} }

View File

@ -4,6 +4,7 @@ import {
compareSemverStrings, compareSemverStrings,
type UpdateCheckResult, type UpdateCheckResult,
} from "../infra/update-check.js"; } from "../infra/update-check.js";
import { formatCliCommand } from "../cli/command-format.js";
import { VERSION } from "../version.js"; import { VERSION } from "../version.js";
export async function getUpdateCheckResult(params: { export async function getUpdateCheckResult(params: {
@ -63,7 +64,7 @@ export function formatUpdateAvailableHint(update: UpdateCheckResult): string | n
details.push(`npm ${availability.latestVersion}`); details.push(`npm ${availability.latestVersion}`);
} }
const suffix = details.length > 0 ? ` (${details.join(" · ")})` : ""; const suffix = details.length > 0 ? ` (${details.join(" · ")})` : "";
return `Update available${suffix}. Run: clawdbot update`; return `Update available${suffix}. Run: ${formatCliCommand("clawdbot update")}`;
} }
export function formatUpdateOneLiner(update: UpdateCheckResult): string { export function formatUpdateOneLiner(update: UpdateCheckResult): string {

View File

@ -1,3 +1,5 @@
import { formatCliCommand } from "../cli/command-format.js";
export function isSystemdUnavailableDetail(detail?: string): boolean { export function isSystemdUnavailableDetail(detail?: string): boolean {
if (!detail) return false; if (!detail) return false;
const normalized = detail.toLowerCase(); const normalized = detail.toLowerCase();
@ -20,6 +22,6 @@ export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}):
} }
return [ return [
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.", "systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
"If you're in a container, run the gateway in the foreground instead of `clawdbot daemon`.", `If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("clawdbot daemon")}\`.`,
]; ];
} }

View File

@ -13,7 +13,7 @@ import { applyMergePatch } from "../../config/merge-patch.js";
import { buildConfigSchema } from "../../config/schema.js"; import { buildConfigSchema } from "../../config/schema.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import { import {
DOCTOR_NONINTERACTIVE_HINT, formatDoctorNonInteractiveHint,
type RestartSentinelPayload, type RestartSentinelPayload,
writeRestartSentinel, writeRestartSentinel,
} from "../../infra/restart-sentinel.js"; } from "../../infra/restart-sentinel.js";
@ -336,7 +336,7 @@ export const configHandlers: GatewayRequestHandlers = {
ts: Date.now(), ts: Date.now(),
sessionKey, sessionKey,
message: note ?? null, message: note ?? null,
doctorHint: DOCTOR_NONINTERACTIVE_HINT, doctorHint: formatDoctorNonInteractiveHint(),
stats: { stats: {
mode: "config.apply", mode: "config.apply",
root: CONFIG_PATH_CLAWDBOT, root: CONFIG_PATH_CLAWDBOT,

View File

@ -1,7 +1,7 @@
import { resolveClawdbotPackageRoot } from "../../infra/clawdbot-root.js"; import { resolveClawdbotPackageRoot } from "../../infra/clawdbot-root.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import { import {
DOCTOR_NONINTERACTIVE_HINT, formatDoctorNonInteractiveHint,
type RestartSentinelPayload, type RestartSentinelPayload,
writeRestartSentinel, writeRestartSentinel,
} from "../../infra/restart-sentinel.js"; } from "../../infra/restart-sentinel.js";
@ -75,7 +75,7 @@ export const updateHandlers: GatewayRequestHandlers = {
ts: Date.now(), ts: Date.now(),
sessionKey, sessionKey,
message: note ?? null, message: note ?? null,
doctorHint: DOCTOR_NONINTERACTIVE_HINT, doctorHint: formatDoctorNonInteractiveHint(),
stats: { stats: {
mode: result.mode, mode: result.mode,
root: result.root ?? undefined, root: result.root ?? undefined,

View File

@ -4,6 +4,7 @@ import { registerSkillsChangeListener } from "../agents/skills/refresh.js";
import type { CanvasHostServer } from "../canvas-host/server.js"; import type { CanvasHostServer } from "../canvas-host/server.js";
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js"; import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
import { createDefaultDeps } from "../cli/deps.js"; import { createDefaultDeps } from "../cli/deps.js";
import { formatCliCommand } from "../cli/command-format.js";
import { import {
CONFIG_PATH_CLAWDBOT, CONFIG_PATH_CLAWDBOT,
isNixMode, isNixMode,
@ -155,7 +156,7 @@ export async function startGatewayServer(
const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed); const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed);
if (!migrated) { if (!migrated) {
throw new Error( throw new Error(
'Legacy config entries detected but auto-migration failed. Run "clawdbot doctor" to migrate.', `Legacy config entries detected but auto-migration failed. Run "${formatCliCommand("clawdbot doctor")}" to migrate.`,
); );
} }
await writeConfigFile(migrated); await writeConfigFile(migrated);
@ -177,7 +178,7 @@ export async function startGatewayServer(
.join("\n") .join("\n")
: "Unknown validation issue."; : "Unknown validation issue.";
throw new Error( throw new Error(
`Invalid config at ${configSnapshot.path}.\n${issues}\nRun "clawdbot doctor" to repair, then retry.`, `Invalid config at ${configSnapshot.path}.\n${issues}\nRun "${formatCliCommand("clawdbot doctor")}" to repair, then retry.`,
); );
} }

View File

@ -11,6 +11,7 @@ import {
} from "../config/config.js"; } from "../config/config.js";
import { runCommandWithTimeout } from "../process/exec.js"; import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { import {
buildDefaultHookUrl, buildDefaultHookUrl,
buildGogWatchServeArgs, buildGogWatchServeArgs,
@ -276,7 +277,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
defaultRuntime.log(`- push endpoint: ${pushEndpoint}`); defaultRuntime.log(`- push endpoint: ${pushEndpoint}`);
defaultRuntime.log(`- hook url: ${hookUrl}`); defaultRuntime.log(`- hook url: ${hookUrl}`);
defaultRuntime.log(`- config: ${CONFIG_PATH_CLAWDBOT}`); defaultRuntime.log(`- config: ${CONFIG_PATH_CLAWDBOT}`);
defaultRuntime.log("Next: clawdbot webhooks gmail run"); defaultRuntime.log(`Next: ${formatCliCommand("clawdbot webhooks gmail run")}`);
} }
export async function runGmailService(opts: GmailRunOptions) { export async function runGmailService(opts: GmailRunOptions) {

View File

@ -1,4 +1,5 @@
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import { formatCliCommand } from "../../cli/command-format.js";
import type { ChannelId, ChannelOutboundTargetMode } from "../../channels/plugins/types.js"; import type { ChannelId, ChannelOutboundTargetMode } from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js"; import type { SessionEntry } from "../../config/sessions.js";
@ -109,7 +110,7 @@ export function resolveOutboundTarget(params: {
return { return {
ok: false, ok: false,
error: new Error( error: new Error(
"Delivering to WebChat is not supported via `clawdbot agent`; use WhatsApp/Telegram or run with --deliver=false.", `Delivering to WebChat is not supported via \`${formatCliCommand("clawdbot agent")}\`; use WhatsApp/Telegram or run with --deliver=false.`,
), ),
}; };
} }

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../cli/command-format.js";
import type { PortListener, PortListenerKind, PortUsage } from "./ports-types.js"; import type { PortListener, PortListenerKind, PortUsage } from "./ports-types.js";
export function classifyPortListener(listener: PortListener, port: number): PortListenerKind { export function classifyPortListener(listener: PortListener, port: number): PortListenerKind {
@ -20,7 +21,7 @@ export function buildPortHints(listeners: PortListener[], port: number): string[
const hints: string[] = []; const hints: string[] = [];
if (kinds.has("gateway")) { if (kinds.has("gateway")) {
hints.push( hints.push(
"Gateway already running locally. Stop it (clawdbot daemon stop) or use a different port.", `Gateway already running locally. Stop it (${formatCliCommand("clawdbot daemon stop")}) or use a different port.`,
); );
} }
if (kinds.has("ssh")) { if (kinds.has("ssh")) {

View File

@ -1,6 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveStateDir } from "../config/paths.js"; import { resolveStateDir } from "../config/paths.js";
export type RestartSentinelLog = { export type RestartSentinelLog = {
@ -44,7 +45,11 @@ export type RestartSentinel = {
const SENTINEL_FILENAME = "restart-sentinel.json"; const SENTINEL_FILENAME = "restart-sentinel.json";
export const DOCTOR_NONINTERACTIVE_HINT = "Run: clawdbot doctor --non-interactive"; export function formatDoctorNonInteractiveHint(
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
): string {
return `Run: ${formatCliCommand("clawdbot doctor --non-interactive", env)}`;
}
export function resolveRestartSentinelPath(env: NodeJS.ProcessEnv = process.env): string { export function resolveRestartSentinelPath(env: NodeJS.ProcessEnv = process.env): string {
return path.join(resolveStateDir(env), SENTINEL_FILENAME); return path.join(resolveStateDir(env), SENTINEL_FILENAME);

View File

@ -4,6 +4,7 @@ import { danger, info, logVerbose, shouldLogVerbose, warn } from "../globals.js"
import { runExec } from "../process/exec.js"; import { runExec } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { colorize, isRich, theme } from "../terminal/theme.js"; import { colorize, isRich, theme } from "../terminal/theme.js";
import { formatCliCommand } from "../cli/command-format.js";
import { ensureBinary } from "./binaries.js"; import { ensureBinary } from "./binaries.js";
function parsePossiblyNoisyJsonObject(stdout: string): Record<string, unknown> { function parsePossiblyNoisyJsonObject(stdout: string): Record<string, unknown> {
@ -268,7 +269,7 @@ export async function ensureFunnel(
runtime.error("Failed to enable Tailscale Funnel. Is it allowed on your tailnet?"); runtime.error("Failed to enable Tailscale Funnel. Is it allowed on your tailnet?");
runtime.error( runtime.error(
info( info(
"Tip: Funnel is optional for CLAWDBOT. You can keep running the web gateway without it: `pnpm clawdbot gateway`", `Tip: Funnel is optional for CLAWDBOT. You can keep running the web gateway without it: \`${formatCliCommand("clawdbot gateway")}\``,
), ),
); );
if (shouldLogVerbose()) { if (shouldLogVerbose()) {

View File

@ -6,6 +6,7 @@ import { resolveStateDir } from "../config/paths.js";
import { resolveClawdbotPackageRoot } from "./clawdbot-root.js"; import { resolveClawdbotPackageRoot } from "./clawdbot-root.js";
import { compareSemverStrings, fetchNpmTagVersion, checkUpdateStatus } from "./update-check.js"; import { compareSemverStrings, fetchNpmTagVersion, checkUpdateStatus } from "./update-check.js";
import { VERSION } from "../version.js"; import { VERSION } from "../version.js";
import { formatCliCommand } from "../cli/command-format.js";
type UpdateCheckState = { type UpdateCheckState = {
lastCheckedAt?: string; lastCheckedAt?: string;
@ -102,7 +103,7 @@ export async function runGatewayUpdateCheck(params: {
state.lastNotifiedVersion !== tagStatus.version || state.lastNotifiedTag !== tag; state.lastNotifiedVersion !== tagStatus.version || state.lastNotifiedTag !== tag;
if (shouldNotify) { if (shouldNotify) {
params.log.info( params.log.info(
`update available (${tag}): v${tagStatus.version} (current v${VERSION}). Run: clawdbot update`, `update available (${tag}): v${tagStatus.version} (current v${VERSION}). Run: ${formatCliCommand("clawdbot update")}`,
); );
nextState.lastNotifiedVersion = tagStatus.version; nextState.lastNotifiedVersion = tagStatus.version;
nextState.lastNotifiedTag = tag; nextState.lastNotifiedTag = tag;

View File

@ -3,6 +3,7 @@ import { ensurePortAvailable, PortInUseError } from "../infra/ports.js";
import { getTailnetHostname } from "../infra/tailscale.js"; import { getTailnetHostname } from "../infra/tailscale.js";
import { logInfo } from "../logger.js"; import { logInfo } from "../logger.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { startMediaServer } from "./server.js"; import { startMediaServer } from "./server.js";
import { saveMediaSource } from "./store.js"; import { saveMediaSource } from "./store.js";
@ -36,7 +37,7 @@ export async function ensureMediaHosted(
if (needsServerStart && !opts.startServer) { if (needsServerStart && !opts.startServer) {
await fs.rm(saved.path).catch(() => {}); await fs.rm(saved.path).catch(() => {});
throw new Error( throw new Error(
"Media hosting requires the webhook/Funnel server. Start `clawdbot webhook`/`clawdbot up` or re-run with --serve-media.", `Media hosting requires the webhook/Funnel server. Start \`${formatCliCommand("clawdbot webhook")}\`/\`${formatCliCommand("clawdbot up")}\` or re-run with --serve-media.`,
); );
} }
if (needsServerStart && opts.startServer) { if (needsServerStart && opts.startServer) {

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../cli/command-format.js";
import type { PairingChannel } from "./pairing-store.js"; import type { PairingChannel } from "./pairing-store.js";
export function buildPairingReply(params: { export function buildPairingReply(params: {
@ -14,6 +15,6 @@ export function buildPairingReply(params: {
`Pairing code: ${code}`, `Pairing code: ${code}`,
"", "",
"Ask the bot owner to approve with:", "Ask the bot owner to approve with:",
`clawdbot pairing approve ${channel} <code>`, formatCliCommand(`clawdbot pairing approve ${channel} <code>`),
].join("\n"); ].join("\n");
} }

View File

@ -1,4 +1,5 @@
import type { OAuthCredentials } from "@mariozechner/pi-ai"; import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { formatCliCommand } from "../cli/command-format.js";
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"; const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai";
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`; const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
@ -28,7 +29,7 @@ export async function refreshQwenPortalCredentials(
const text = await response.text(); const text = await response.text();
if (response.status === 400) { if (response.status === 400) {
throw new Error( throw new Error(
"Qwen OAuth refresh token expired or invalid. Re-authenticate with `clawdbot models auth login --provider qwen-portal`.", `Qwen OAuth refresh token expired or invalid. Re-authenticate with \`${formatCliCommand("clawdbot models auth login --provider qwen-portal")}\`.`,
); );
} }
throw new Error(`Qwen OAuth refresh failed: ${text || response.statusText}`); throw new Error(`Qwen OAuth refresh failed: ${text || response.statusText}`);

View File

@ -7,6 +7,7 @@ import type { ClawdbotConfig, ConfigFileSnapshot } from "../config/config.js";
import { createConfigIO } from "../config/config.js"; import { createConfigIO } from "../config/config.js";
import { resolveNativeSkillsEnabled } from "../config/commands.js"; import { resolveNativeSkillsEnabled } from "../config/commands.js";
import { resolveOAuthDir } from "../config/paths.js"; import { resolveOAuthDir } from "../config/paths.js";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js"; import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js";
import { normalizeAgentId } from "../routing/session-key.js"; import { normalizeAgentId } from "../routing/session-key.js";
@ -105,7 +106,7 @@ export function collectSyncedFolderFindings(params: {
severity: "warn", severity: "warn",
title: "State/config path looks like a synced folder", title: "State/config path looks like a synced folder",
detail: `stateDir=${params.stateDir}, configPath=${params.configPath}. Synced folders (iCloud/Dropbox/OneDrive/Google Drive) can leak tokens and transcripts onto other devices.`, detail: `stateDir=${params.stateDir}, configPath=${params.configPath}. Synced folders (iCloud/Dropbox/OneDrive/Google Drive) can leak tokens and transcripts onto other devices.`,
remediation: `Keep CLAWDBOT_STATE_DIR on a local-only volume and re-run "clawdbot security audit --fix".`, remediation: `Keep CLAWDBOT_STATE_DIR on a local-only volume and re-run "${formatCliCommand("clawdbot security audit --fix")}".`,
}); });
} }
return findings; return findings;

View File

@ -5,6 +5,7 @@ import type { ClawdbotConfig } from "../config/config.js";
import { resolveBrowserConfig } from "../browser/config.js"; import { resolveBrowserConfig } from "../browser/config.js";
import { resolveConfigPath, resolveStateDir } from "../config/paths.js"; import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
import { resolveGatewayAuth } from "../gateway/auth.js"; import { resolveGatewayAuth } from "../gateway/auth.js";
import { formatCliCommand } from "../cli/command-format.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { probeGateway } from "../gateway/probe.js"; import { probeGateway } from "../gateway/probe.js";
import { import {
@ -264,7 +265,7 @@ function collectBrowserControlFindings(cfg: ClawdbotConfig): SecurityAuditFindin
severity: "warn", severity: "warn",
title: "Browser control config looks invalid", title: "Browser control config looks invalid",
detail: String(err), detail: String(err),
remediation: `Fix browser.controlUrl/browser.cdpUrl in ${resolveConfigPath()} and re-run "clawdbot security audit --deep".`, remediation: `Fix browser.controlUrl/browser.cdpUrl in ${resolveConfigPath()} and re-run "${formatCliCommand("clawdbot security audit --deep")}".`,
}); });
return findings; return findings;
} }
@ -840,7 +841,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<Secu
severity: "warn", severity: "warn",
title: "Gateway probe failed (deep)", title: "Gateway probe failed (deep)",
detail: deep.gateway.error ?? "gateway unreachable", detail: deep.gateway.error ?? "gateway unreachable",
remediation: `Run "clawdbot status --all" to debug connectivity/auth, then re-run "clawdbot security audit --deep".`, remediation: `Run "${formatCliCommand("clawdbot status --all")}" to debug connectivity/auth, then re-run "${formatCliCommand("clawdbot security audit --deep")}".`,
}); });
} }

View File

@ -12,6 +12,7 @@ import {
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js";
import { formatLocationText, toLocationContext } from "../channels/location.js"; import { formatLocationText, toLocationContext } from "../channels/location.js";
import { formatCliCommand } from "../cli/command-format.js";
import { import {
readSessionUpdatedAt, readSessionUpdatedAt,
recordSessionMetaFromInbound, recordSessionMetaFromInbound,
@ -234,7 +235,7 @@ export const buildTelegramMessageContext = async ({
`Pairing code: ${code}`, `Pairing code: ${code}`,
"", "",
"Ask the bot owner to approve with:", "Ask the bot owner to approve with:",
"clawdbot pairing approve telegram <code>", formatCliCommand("clawdbot pairing approve telegram <code>"),
].join("\n"), ].join("\n"),
); );
} }

View File

@ -1,3 +1,4 @@
import { formatCliCommand } from "../cli/command-format.js";
import type { PollInput } from "../polls.js"; import type { PollInput } from "../polls.js";
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
@ -42,7 +43,7 @@ export function requireActiveWebListener(accountId?: string | null): {
const listener = listeners.get(id) ?? null; const listener = listeners.get(id) ?? null;
if (!listener) { if (!listener) {
throw new Error( throw new Error(
`No active WhatsApp Web listener (account: ${id}). Start the gateway, then link WhatsApp with: clawdbot channels login --channel whatsapp --account ${id}.`, `No active WhatsApp Web listener (account: ${id}). Start the gateway, then link WhatsApp with: ${formatCliCommand(`clawdbot channels login --channel whatsapp --account ${id}`)}.`,
); );
} }
return { accountId: id, listener }; return { accountId: id, listener };

Some files were not shown because too many files have changed in this diff Show More