diff --git a/docs/concepts/session.md b/docs/concepts/session.md index 3a586f860..8493a9c20 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -117,8 +117,8 @@ Send these as standalone messages so they register. ``` ## Inspecting -- `pnpm clawdbot status` — shows store path and recent sessions. -- `pnpm clawdbot sessions --json` — dumps every entry (filter with `--active `). +- `clawdbot status` — shows store path and recent sessions. +- `clawdbot sessions --json` — dumps every entry (filter with `--active `). - `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 `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors). diff --git a/docs/debug/node-issue.md b/docs/debug/node-issue.md index 575337c65..6feddeea2 100644 --- a/docs/debug/node-issue.md +++ b/docs/debug/node-issue.md @@ -48,7 +48,7 @@ node --import tsx scripts/repro/tsx-name-repro.ts ## Regression history - `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 - Use Bun for dev scripts (current temporary revert). diff --git a/docs/debugging.md b/docs/debugging.md index fd754e502..a7f8a85ff 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -59,9 +59,11 @@ Recommended flow (dev profile + dev bootstrap): ```bash pnpm gateway:dev -CLAWDBOT_PROFILE=dev pnpm clawdbot tui +CLAWDBOT_PROFILE=dev clawdbot tui ``` +If you don’t have a global install yet, run the CLI via `pnpm clawdbot ...`. + What this does: 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: ```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 diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index 0b3c6496a..0c1c60403 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -378,7 +378,7 @@ clawdbot channels login ### Build errors on `main` — what’s the standard fix path? 1) `git pull origin main && pnpm install` -2) `pnpm clawdbot doctor` +2) `clawdbot doctor` 3) Check GitHub issues or Discord 4) Temporary workaround: check out an older commit @@ -392,7 +392,7 @@ Typical recovery: git status # ensure you’re in the repo root pnpm install pnpm build -pnpm clawdbot doctor +clawdbot doctor clawdbot daemon restart ``` diff --git a/docs/index.md b/docs/index.md index a1b4d1507..293609031 100644 --- a/docs/index.md +++ b/docs/index.md @@ -123,9 +123,11 @@ cd clawdbot pnpm install pnpm ui:build # auto-installs UI deps on first run pnpm build -pnpm clawdbot onboard --install-daemon +clawdbot onboard --install-daemon ``` +If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo. + Multi-instance quickstart (optional): ```bash diff --git a/docs/install/index.md b/docs/install/index.md index 4d90de418..467ea6bf0 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -7,40 +7,43 @@ read_when: # 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`, it’s usually a Node/npm PATH issue (global npm bin dir not on PATH). See the section below. - -## 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 can’t 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) +## Quick install (recommended) ```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 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 ``` -## 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 don’t 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: @@ -92,28 +148,28 @@ Equivalent env vars (useful for automation): - `CLAWDBOT_NO_ONBOARD=1` - `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 -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 can’t find global npm binaries (including `clawdbot`). + +Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`): ```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 -pnpm add -g clawdbot@latest -``` +## Update / uninstall -Then: - -```bash -clawdbot onboard --install-daemon -``` +- Updates: [Updating](/install/updating) +- Uninstall: [Uninstall](/install/uninstall) diff --git a/docs/install/uninstall.md b/docs/install/uninstall.md index 8a51ea6a2..c179438a1 100644 --- a/docs/install/uninstall.md +++ b/docs/install/uninstall.md @@ -118,7 +118,7 @@ Remove it with `npm rm -g clawdbot` (or `pnpm remove -g` / `bun remove -g` if yo ### 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). 2) Delete the repo directory. diff --git a/docs/install/updating.md b/docs/install/updating.md index 66671be17..7bb6fc16a 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -119,12 +119,13 @@ git pull pnpm install pnpm build pnpm ui:build # auto-installs UI deps on first run -pnpm clawdbot doctor -pnpm clawdbot health +clawdbot doctor +clawdbot health ``` 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/`. +- 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. - 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. diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index 9f74f26e2..040a28989 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -155,7 +155,7 @@ Options: - `--timeout `: overall discovery window (default `2000`) - `--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 app’s discovery pipeline (NWBrowser + tailnet DNS‑SD fallback) differs from the Node CLI’s `dns-sd` based discovery. diff --git a/docs/platforms/windows.md b/docs/platforms/windows.md index a65534440..1c97631a5 100644 --- a/docs/platforms/windows.md +++ b/docs/platforms/windows.md @@ -97,7 +97,7 @@ cd clawdbot pnpm install pnpm ui:build # auto-installs UI deps on first run pnpm build -pnpm clawdbot onboard +clawdbot onboard ``` Full guide: [Getting Started](/start/getting-started) diff --git a/docs/start/faq.md b/docs/start/faq.md index 7d6824eb5..f62e0315f 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -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: ```bash -git clone https://github.com/clawdbot/clawdbot.git -cd clawdbot - -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 +curl -fsSL https://clawd.bot/install.sh | bash +clawdbot onboard --install-daemon ``` 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 don’t have a global install yet, run it via `pnpm clawdbot onboard`. + ### 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 didn’t 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 pnpm install pnpm build -pnpm clawdbot doctor +clawdbot doctor clawdbot daemon restart ``` diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md index 2ae176c25..aa890eac5 100644 --- a/docs/start/getting-started.md +++ b/docs/start/getting-started.md @@ -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 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 ### WhatsApp (QR login) @@ -158,9 +165,11 @@ cd clawdbot pnpm install pnpm ui:build # auto-installs UI deps on first run pnpm build -pnpm clawdbot onboard --install-daemon +clawdbot onboard --install-daemon ``` +If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo. + Gateway (from this repo): ```bash @@ -169,15 +178,13 @@ node dist/entry.js gateway --port 18789 --verbose ## 7) Verify end-to-end -In a new terminal: +In a new terminal, send a test message: ```bash -clawdbot status -clawdbot health 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 won’t 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 won’t be able to respond without it. 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. diff --git a/docs/start/setup.md b/docs/start/setup.md index f8a41f860..587b7fd6b 100644 --- a/docs/start/setup.md +++ b/docs/start/setup.md @@ -35,9 +35,11 @@ clawdbot setup From inside this repo, use the local CLI entry: ```bash -pnpm clawdbot setup +clawdbot setup ``` +If you don’t have a global install yet, run it via `pnpm clawdbot setup`. + ## Stable workflow (macOS app first) 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: ```bash -pnpm clawdbot health +clawdbot health ``` ### Common footguns diff --git a/docs/testing.md b/docs/testing.md index 26bd4d132..e336ecef5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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: ```bash -pnpm clawdbot models list -pnpm clawdbot models list --json +clawdbot models list +clawdbot models list --json ``` ## Live: Anthropic setup-token smoke diff --git a/src/agents/auth-profiles/doctor.ts b/src/agents/auth-profiles/doctor.ts index cb9ca5d6a..117bf320e 100644 --- a/src/agents/auth-profiles/doctor.ts +++ b/src/agents/auth-profiles/doctor.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../../cli/command-format.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { normalizeProviderId } from "../model-selection.js"; import { listProfilesForProvider } from "./profiles.js"; @@ -37,6 +38,6 @@ export function formatAuthDoctorHint(params: { }`, `- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`, `- suggested profile: ${suggested}`, - 'Fix: run "clawdbot doctor --yes"', + `Fix: run "${formatCliCommand("clawdbot doctor --yes")}"`, ].join("\n"); } diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 8e53bd21c..e434f7dac 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -4,6 +4,7 @@ import { type Api, getEnvApiKey, type Model } from "@mariozechner/pi-ai"; import type { ClawdbotConfig } from "../config/config.js"; import type { ModelProviderConfig } from "../config/types.js"; import { getShellEnvAppliedKeys } from "../infra/shell-env.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { type AuthProfileStore, ensureAuthProfileStore, @@ -103,7 +104,7 @@ export async function resolveApiKeyForProvider(params: { [ `No API key found for provider "${provider}".`, `Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`, - "Configure auth for this agent (clawdbot agents add ) or copy auth-profiles.json from the main agentDir.", + `Configure auth for this agent (${formatCliCommand("clawdbot agents add ")}) or copy auth-profiles.json from the main agentDir.`, ].join(" "), ); } diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index 2745b3149..c58e82b13 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -1,6 +1,7 @@ import { spawn } from "node:child_process"; import { defaultRuntime } from "../../runtime.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { DEFAULT_SANDBOX_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js"; import { readRegistry, updateRegistry } from "./registry.js"; import { computeSandboxConfigHash } from "./config-hash.js"; @@ -214,13 +215,13 @@ async function readContainerConfigHash(containerName: string): Promise" --from-identity +Run: ${formatCliCommand('clawdbot agents set-identity --workspace "" --from-identity')} If multiple agents share a host, add --agent . ## Cleanup diff --git a/src/auto-reply/reply/bash-command.ts b/src/auto-reply/reply/bash-command.ts index 0e81c22ce..a83bf3952 100644 --- a/src/auto-reply/reply/bash-command.ts +++ b/src/auto-reply/reply/bash-command.ts @@ -4,6 +4,7 @@ import { createExecTool } from "../../agents/bash-tools.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { killProcessTree } from "../../agents/shell-utils.js"; import type { ClawdbotConfig } from "../../config/config.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { logVerbose } from "../../globals.js"; import { clampInt } from "../../utils.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.allowFrom."); 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"); } diff --git a/src/auto-reply/reply/directive-handling.shared.ts b/src/auto-reply/reply/directive-handling.shared.ts index 07da3cb31..961fe50a7 100644 --- a/src/auto-reply/reply/directive-handling.shared.ts +++ b/src/auto-reply/reply/directive-handling.shared.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../../cli/command-format.js"; import type { ElevatedLevel, ReasoningLevel } from "./directives.js"; export const SYSTEM_MARK = "⚙️"; @@ -44,7 +45,9 @@ export function formatElevatedUnavailableText(params: { ); } 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"); } diff --git a/src/auto-reply/reply/reply-elevated.ts b/src/auto-reply/reply/reply-elevated.ts index 40c222f89..121c5f88a 100644 --- a/src/auto-reply/reply/reply-elevated.ts +++ b/src/auto-reply/reply/reply-elevated.ts @@ -4,6 +4,7 @@ import { normalizeChannelId } from "../../channels/plugins/index.js"; import { CHAT_CHANNEL_ORDER } from "../../channels/registry.js"; import type { AgentElevatedAllowFromConfig, ClawdbotConfig } from "../../config/config.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import type { MsgContext } from "../templating.js"; 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.allowFrom."); 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"); } diff --git a/src/browser/client-fetch.ts b/src/browser/client-fetch.ts index 8aebd539d..e815f45c4 100644 --- a/src/browser/client-fetch.ts +++ b/src/browser/client-fetch.ts @@ -1,5 +1,6 @@ import { extractErrorCode, formatErrorMessage } from "../infra/errors.js"; import { loadConfig } from "../config/config.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { resolveBrowserConfig } from "./config.js"; let cachedConfigToken: string | null | undefined = undefined; @@ -30,8 +31,7 @@ function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): const cause = unwrapCause(err); const code = extractErrorCode(cause) ?? extractErrorCode(err) ?? ""; - const hint = - "Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or `clawdbot gateway`) and try again."; + const hint = `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`; if (code === "ECONNREFUSED") { return new Error( diff --git a/src/browser/pw-tools-core.responses.ts b/src/browser/pw-tools-core.responses.ts index d2e477d2c..b283fe6b1 100644 --- a/src/browser/pw-tools-core.responses.ts +++ b/src/browser/pw-tools-core.responses.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../cli/command-format.js"; import { ensurePageState, getPageForTargetId } from "./pw-session.js"; import { normalizeTimeoutMs } from "./pw-tools-core.shared.js"; @@ -65,7 +66,7 @@ export async function responseBodyViaPlaywright(opts: { cleanup(); reject( 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); diff --git a/src/channels/plugins/helpers.ts b/src/channels/plugins/helpers.ts index 25632bc15..43f19b536 100644 --- a/src/channels/plugins/helpers.ts +++ b/src/channels/plugins/helpers.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../../cli/command-format.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import type { ChannelPlugin } from "./types.js"; @@ -13,5 +14,7 @@ export function resolveChannelDefaultAccountId(params: { } export function formatPairingApproveHint(channelId: string): string { - return `Approve via: clawdbot pairing list ${channelId} / clawdbot pairing approve ${channelId} `; + const listCmd = formatCliCommand(`clawdbot pairing list ${channelId}`); + const approveCmd = formatCliCommand(`clawdbot pairing approve ${channelId} `); + return `Approve via: ${listCmd} / ${approveCmd}`; } diff --git a/src/channels/plugins/onboarding/signal.ts b/src/channels/plugins/onboarding/signal.ts index d8ec0b667..d75577ad1 100644 --- a/src/channels/plugins/onboarding/signal.ts +++ b/src/channels/plugins/onboarding/signal.ts @@ -9,6 +9,7 @@ import { resolveSignalAccount, } from "../../../signal/accounts.js"; import { formatDocsLink } from "../../../terminal/links.js"; +import { formatCliCommand } from "../../../cli/command-format.js"; import { normalizeE164 } from "../../../utils.js"; import type { WizardPrompter } from "../../../wizard/prompts.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"', "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")}`, ].join("\n"), "Signal next steps", diff --git a/src/channels/plugins/onboarding/telegram.ts b/src/channels/plugins/onboarding/telegram.ts index 9150887c9..0356acd33 100644 --- a/src/channels/plugins/onboarding/telegram.ts +++ b/src/channels/plugins/onboarding/telegram.ts @@ -7,6 +7,7 @@ import { resolveTelegramAccount, } from "../../../telegram/accounts.js"; import { formatDocsLink } from "../../../terminal/links.js"; +import { formatCliCommand } from "../../../cli/command-format.js"; import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; import { addWildcardAllowFrom, promptAccountId } from "./helpers.js"; @@ -46,7 +47,7 @@ async function noteTelegramTokenHelp(prompter: WizardPrompter): Promise { async function noteTelegramUserIdHelp(prompter: WizardPrompter): Promise { 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/getUpdates and read message.from.id", "3) Third-party: DM @userinfobot or @getidsbot", `Docs: ${formatDocsLink("/telegram")}`, diff --git a/src/channels/plugins/onboarding/whatsapp.ts b/src/channels/plugins/onboarding/whatsapp.ts index 21e78c509..06528c2f1 100644 --- a/src/channels/plugins/onboarding/whatsapp.ts +++ b/src/channels/plugins/onboarding/whatsapp.ts @@ -7,6 +7,7 @@ import type { DmPolicy } from "../../../config/types.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js"; import type { RuntimeEnv } from "../../../runtime.js"; import { formatDocsLink } from "../../../terminal/links.js"; +import { formatCliCommand } from "../../../cli/command-format.js"; import { normalizeE164 } from "../../../utils.js"; import { listWhatsAppAccountIds, @@ -321,7 +322,10 @@ export const whatsappOnboardingAdapter: ChannelOnboardingAdapter = { await prompter.note(`Docs: ${formatDocsLink("/whatsapp", "whatsapp")}`, "WhatsApp help"); } } 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, { diff --git a/src/channels/plugins/status-issues/whatsapp.ts b/src/channels/plugins/status-issues/whatsapp.ts index 5b2b2ca7a..000c20604 100644 --- a/src/channels/plugins/status-issues/whatsapp.ts +++ b/src/channels/plugins/status-issues/whatsapp.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../../../cli/command-format.js"; import type { ChannelAccountSnapshot, ChannelStatusIssue } from "../types.js"; import { asString, isRecord } from "./shared.js"; @@ -47,7 +48,7 @@ export function collectWhatsAppStatusIssues( accountId, kind: "auth", 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; } @@ -58,7 +59,7 @@ export function collectWhatsAppStatusIssues( accountId, kind: "runtime", 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.`, }); } } diff --git a/src/cli/browser-cli-extension.ts b/src/cli/browser-cli-extension.ts index 991dd5abf..ba4c62a65 100644 --- a/src/cli/browser-cli-extension.ts +++ b/src/cli/browser-cli-extension.ts @@ -11,6 +11,7 @@ import { defaultRuntime } from "../runtime.js"; import { movePathToTrash } from "../browser/trash.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; +import { formatCliCommand } from "./command-format.js"; function bundledExtensionRootDir() { const here = path.dirname(fileURLToPath(import.meta.url)); @@ -103,7 +104,7 @@ export function registerBrowserExtensionCommands( defaultRuntime.error( 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")}`, ].join("\n"), ), diff --git a/src/cli/browser-cli.ts b/src/cli/browser-cli.ts index cfa97e602..1193b2f80 100644 --- a/src/cli/browser-cli.ts +++ b/src/cli/browser-cli.ts @@ -4,6 +4,7 @@ import { danger } from "../globals.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; +import { formatCliCommand } from "./command-format.js"; import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js"; import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js"; import { registerBrowserDebugCommands } from "./browser-cli-debug.js"; @@ -32,7 +33,9 @@ export function registerBrowserCli(program: Command) { ) .action(() => { browser.outputHelp(); - defaultRuntime.error(danger('Missing subcommand. Try: "clawdbot browser status"')); + defaultRuntime.error( + danger(`Missing subcommand. Try: "${formatCliCommand("clawdbot browser status")}"`), + ); defaultRuntime.exit(1); }); diff --git a/src/cli/command-format.ts b/src/cli/command-format.ts new file mode 100644 index 000000000..81e791b67 --- /dev/null +++ b/src/cli/command-format.ts @@ -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 = process.env as Record, +): 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}`); +} diff --git a/src/cli/config-cli.ts b/src/cli/config-cli.ts index 22076a869..22fa7e7c3 100644 --- a/src/cli/config-cli.ts +++ b/src/cli/config-cli.ts @@ -5,6 +5,7 @@ import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js"; import { danger, info } from "../globals.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; +import { formatCliCommand } from "./command-format.js"; import { theme } from "../terminal/theme.js"; type PathSegment = string; @@ -171,7 +172,7 @@ async function loadValidConfig() { for (const issue of snapshot.issues) { defaultRuntime.error(`- ${issue.path || ""}: ${issue.message}`); } - defaultRuntime.error("Run `clawdbot doctor` to repair, then retry."); + defaultRuntime.error(`Run \`${formatCliCommand("clawdbot doctor")}\` to repair, then retry.`); defaultRuntime.exit(1); return snapshot; } diff --git a/src/cli/daemon-cli/install.ts b/src/cli/daemon-cli/install.ts index 169451b92..cf852fd38 100644 --- a/src/cli/daemon-cli/install.ts +++ b/src/cli/daemon-cli/install.ts @@ -7,6 +7,7 @@ import { loadConfig, resolveGatewayPort } from "../../config/config.js"; import { resolveIsNixMode } from "../../config/paths.js"; import { resolveGatewayService } from "../../daemon/service.js"; import { defaultRuntime } from "../../runtime.js"; +import { formatCliCommand } from "../command-format.js"; import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js"; import { parsePort } from "./shared.js"; import type { DaemonInstallOptions } from "./types.js"; @@ -82,7 +83,9 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { }); if (!json) { 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; } diff --git a/src/cli/daemon-cli/shared.ts b/src/cli/daemon-cli/shared.ts index 22483e33a..807cf687a 100644 --- a/src/cli/daemon-cli/shared.ts +++ b/src/cli/daemon-cli/shared.ts @@ -5,6 +5,7 @@ import { } from "../../daemon/constants.js"; import { resolveGatewayLogPaths } from "../../daemon/launchd.js"; import { getResolvedLoggerSettings } from "../../logging.js"; +import { formatCliCommand } from "../command-format.js"; export function parsePort(raw: unknown): number | null { if (raw === undefined || raw === null) return null; @@ -122,7 +123,7 @@ export function renderRuntimeHints( } })(); 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}`); return hints; } @@ -144,7 +145,10 @@ export function renderRuntimeHints( } 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; switch (process.platform) { case "darwin": { diff --git a/src/cli/daemon-cli/status.print.ts b/src/cli/daemon-cli/status.print.ts index 43391da0a..b4e879666 100644 --- a/src/cli/daemon-cli/status.print.ts +++ b/src/cli/daemon-cli/status.print.ts @@ -13,6 +13,7 @@ import { isWSLEnv } from "../../infra/wsl.js"; import { getResolvedLoggerSettings } from "../../logging.js"; import { defaultRuntime } from "../../runtime.js"; import { colorize, isRich, theme } from "../../terminal/theme.js"; +import { formatCliCommand } from "../command-format.js"; import { formatRuntimeStatus, renderRuntimeHints, safeDaemonEnv } from "./shared.js"; import { 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('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( 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}`, ), ); - defaultRuntime.error(errorText("Then reinstall: clawdbot daemon install")); + defaultRuntime.error( + errorText(`Then reinstall: ${formatCliCommand("clawdbot daemon install")}`), + ); spacer(); } @@ -259,7 +264,7 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) for (const svc of legacyServices) { defaultRuntime.error(`- ${errorText(svc.label)} (${svc.detail})`); } - defaultRuntime.error(errorText("Cleanup: clawdbot doctor")); + defaultRuntime.error(errorText(`Cleanup: ${formatCliCommand("clawdbot doctor")}`)); spacer(); } @@ -288,6 +293,6 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) 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`); } diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index ede01fa1f..211b4c2c4 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -18,6 +18,7 @@ import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js"; import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { defaultRuntime } from "../../runtime.js"; +import { formatCliCommand } from "../command-format.js"; import { forceFreePortAndWait } from "../ports.js"; import { ensureDevGatewayConfig } from "./dev.js"; import { runGatewayLoop } from "./run-loop.js"; @@ -161,7 +162,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) { if (!opts.allowUnconfigured && mode !== "local") { if (!configExists) { 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 { defaultRuntime.error( @@ -277,7 +278,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) { ) { const errMessage = describeUnknownError(err); 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 { const diagnostics = await inspectPortUsage(port); diff --git a/src/cli/gateway-cli/shared.ts b/src/cli/gateway-cli/shared.ts index 9694e1cde..3105a94e6 100644 --- a/src/cli/gateway-cli/shared.ts +++ b/src/cli/gateway-cli/shared.ts @@ -5,6 +5,7 @@ import { } from "../../daemon/constants.js"; import { resolveGatewayService } from "../../daemon/service.js"; import { defaultRuntime } from "../../runtime.js"; +import { formatCliCommand } from "../command-format.js"; export function parsePort(raw: unknown): number | null { if (raw === undefined || raw === null) return null; @@ -67,21 +68,21 @@ export function renderGatewayServiceStopHints(env: NodeJS.ProcessEnv = process.e switch (process.platform) { case "darwin": return [ - "Tip: clawdbot daemon stop", + `Tip: ${formatCliCommand("clawdbot daemon stop")}`, `Or: launchctl bootout gui/$UID/${resolveGatewayLaunchAgentLabel(profile)}`, ]; case "linux": return [ - "Tip: clawdbot daemon stop", + `Tip: ${formatCliCommand("clawdbot daemon stop")}`, `Or: systemctl --user stop ${resolveGatewaySystemdServiceName(profile)}.service`, ]; case "win32": return [ - "Tip: clawdbot daemon stop", + `Tip: ${formatCliCommand("clawdbot daemon stop")}`, `Or: schtasks /End /TN "${resolveGatewayWindowsTaskName(profile)}"`, ]; default: - return ["Tip: clawdbot daemon stop"]; + return [`Tip: ${formatCliCommand("clawdbot daemon stop")}`]; } } diff --git a/src/cli/hooks-cli.ts b/src/cli/hooks-cli.ts index 5105e81e0..b7fa2f37b 100644 --- a/src/cli/hooks-cli.ts +++ b/src/cli/hooks-cli.ts @@ -24,6 +24,7 @@ import { buildPluginStatusReport } from "../plugins/status.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; +import { formatCliCommand } from "./command-format.js"; import { resolveUserPath } from "../utils.js"; export type HooksListOptions = { @@ -150,7 +151,7 @@ export function formatHooksList(report: HookStatusReport, opts: HooksListOptions if (hooks.length === 0) { 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."; return message; } @@ -194,7 +195,7 @@ export function formatHookInfo( if (opts.json) { 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) { diff --git a/src/cli/logs-cli.ts b/src/cli/logs-cli.ts index 343fd4c3b..6c5bccb53 100644 --- a/src/cli/logs-cli.ts +++ b/src/cli/logs-cli.ts @@ -5,6 +5,7 @@ import { parseLogLine } from "../logging/parse-log-line.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; +import { formatCliCommand } from "./command-format.js"; import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js"; type LogsTailPayload = { @@ -117,7 +118,7 @@ function emitGatewayError( ) { const details = buildGatewayConnectionDetails({ url: opts.url }); 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); if (mode === "json") { diff --git a/src/cli/node-cli/daemon.ts b/src/cli/node-cli/daemon.ts index e90dd2387..111ac510e 100644 --- a/src/cli/node-cli/daemon.ts +++ b/src/cli/node-cli/daemon.ts @@ -18,6 +18,7 @@ import { isWSL } from "../../infra/wsl.js"; import { loadNodeHostConfig } from "../../node-host/config.js"; import { defaultRuntime } from "../../runtime.js"; import { colorize, isRich, theme } from "../../terminal/theme.js"; +import { formatCliCommand } from "../command-format.js"; import { buildDaemonServiceSnapshot, createNullWriter, @@ -46,7 +47,10 @@ type NodeDaemonStatusOptions = { }; 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) { case "darwin": return [ @@ -168,7 +172,9 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) { }); if (!json) { 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; } diff --git a/src/cli/pairing-cli.ts b/src/cli/pairing-cli.ts index 1582accc3..d0ecbce14 100644 --- a/src/cli/pairing-cli.ts +++ b/src/cli/pairing-cli.ts @@ -10,6 +10,7 @@ import { } from "../pairing/pairing-store.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; +import { formatCliCommand } from "./command-format.js"; /** Parse channel, allowing extension channels not in core registry. */ function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel { @@ -95,12 +96,12 @@ export function registerPairingCli(program: Command) { const resolvedCode = opts.channel ? codeOrChannel : code; if (!opts.channel && !code) { throw new Error( - `Usage: clawdbot pairing approve (or: clawdbot pairing approve --channel )`, + `Usage: ${formatCliCommand("clawdbot pairing approve ")} (or: ${formatCliCommand("clawdbot pairing approve --channel ")})`, ); } if (opts.channel && code != null) { throw new Error( - `Too many arguments. Use: clawdbot pairing approve --channel `, + `Too many arguments. Use: ${formatCliCommand("clawdbot pairing approve --channel ")}`, ); } const channel = parseChannel(channelRaw, channels); diff --git a/src/cli/profile-utils.ts b/src/cli/profile-utils.ts new file mode 100644 index 000000000..cef12a3b0 --- /dev/null +++ b/src/cli/profile-utils.ts @@ -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; +} diff --git a/src/cli/profile.test.ts b/src/cli/profile.test.ts index d3fb90052..0470bcf2c 100644 --- a/src/cli/profile.test.ts +++ b/src/cli/profile.test.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; +import { formatCliCommand } from "./command-format.js"; import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js"; describe("parseCliProfileArgs", () => { @@ -76,3 +77,63 @@ describe("applyCliProfileEnv", () => { 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", + ); + }); +}); diff --git a/src/cli/profile.ts b/src/cli/profile.ts index 3f3522c05..90f9678dc 100644 --- a/src/cli/profile.ts +++ b/src/cli/profile.ts @@ -1,6 +1,8 @@ import os from "node:os"; import path from "node:path"; +import { isValidProfileName } from "./profile-utils.js"; + export type CliProfileParseResult = | { ok: true; profile: string | null; argv: string[] } | { ok: false; error: string }; @@ -21,12 +23,6 @@ function takeValue( 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 { if (argv.length < 2) return { ok: true, profile: null, argv }; diff --git a/src/cli/program/config-guard.ts b/src/cli/program/config-guard.ts index 17429a285..bc3d4be8b 100644 --- a/src/cli/program/config-guard.ts +++ b/src/cli/program/config-guard.ts @@ -4,6 +4,7 @@ import { loadAndMaybeMigrateDoctorConfig } from "../../commands/doctor-config-fl import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { loadClawdbotPlugins } from "../../plugins/loader.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"]); @@ -72,7 +73,9 @@ export async function ensureConfigReady(params: { params.runtime.error(pluginIssues.map((issue) => ` ${error(issue)}`).join("\n")); } params.runtime.error(""); - params.runtime.error(`${muted("Run:")} ${commandText("clawdbot doctor --fix")}`); + params.runtime.error( + `${muted("Run:")} ${commandText(formatCliCommand("clawdbot doctor --fix"))}`, + ); if (!allowInvalid) { params.runtime.exit(1); } diff --git a/src/cli/security-cli.ts b/src/cli/security-cli.ts index bdf0303fa..31311022d 100644 --- a/src/cli/security-cli.ts +++ b/src/cli/security-cli.ts @@ -7,6 +7,7 @@ import { runSecurityAudit } from "../security/audit.js"; import { fixSecurityFootguns } from "../security/fix.js"; import { formatDocsLink } from "../terminal/links.js"; import { isRich, theme } from "../terminal/theme.js"; +import { formatCliCommand } from "./command-format.js"; type SecurityAuditOptions = { json?: boolean; @@ -67,10 +68,10 @@ export function registerSecurityCli(program: Command) { const lines: string[] = []; lines.push(heading("Clawdbot security audit")); 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) { - lines.push(muted(`Fix: clawdbot security audit --fix`)); + lines.push(muted(`Fix: ${formatCliCommand("clawdbot security audit --fix")}`)); if (!fixResult) { lines.push(muted("Fixes: failed to apply (unexpected error)")); } else if ( diff --git a/src/cli/skills-cli.ts b/src/cli/skills-cli.ts index 8acb11b07..42aa1680a 100644 --- a/src/cli/skills-cli.ts +++ b/src/cli/skills-cli.ts @@ -10,6 +10,7 @@ import { loadConfig } from "../config/config.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; +import { formatCliCommand } from "./command-format.js"; export type SkillsListOptions = { json?: boolean; @@ -101,7 +102,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti if (skills.length === 0) { 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."; return appendClawdHubHint(message, opts.json); } @@ -148,7 +149,7 @@ export function formatSkillInfo( return JSON.stringify({ error: "not found", skill: skillName }, null, 2); } 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, ); } diff --git a/src/cli/update-cli.ts b/src/cli/update-cli.ts index 6f893f8cd..1d8bad644 100644 --- a/src/cli/update-cli.ts +++ b/src/cli/update-cli.ts @@ -15,6 +15,7 @@ import { } from "../infra/update-runner.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; +import { formatCliCommand } from "./command-format.js"; import { stylePromptMessage } from "../terminal/prompt-style.js"; import { theme } from "../terminal/theme.js"; @@ -373,7 +374,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { if (result.reason === "not-git-install") { defaultRuntime.log( 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( @@ -410,7 +411,9 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { if (!opts.json) { defaultRuntime.log(theme.warn(`Daemon restart failed: ${String(err)}`)); 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 { if (result.mode === "npm" || result.mode === "pnpm") { defaultRuntime.log( 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 { 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.`, + ), ); } } diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index d43598d61..0ba21874a 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -7,6 +7,7 @@ import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { listAgentIds } from "../agents/agent-scope.js"; import { normalizeAgentId } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, @@ -92,7 +93,7 @@ export async function agentViaGatewayCommand(opts: AgentCliOpts, runtime: Runtim const knownAgents = listAgentIds(cfg); if (!knownAgents.includes(agentId)) { 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.`, ); } } diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 63258a4b8..23a2060d0 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -47,6 +47,7 @@ import { } from "../infra/agent-events.js"; import { getRemoteSkillEligibility } from "../infra/skills-remote.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { applyVerboseOverride } from "../sessions/level-overrides.js"; import { resolveSendPolicy } from "../sessions/send-policy.js"; import { resolveMessageChannel } from "../utils/message-channel.js"; @@ -75,7 +76,7 @@ export async function agentCommand( const knownAgents = listAgentIds(cfg); if (!knownAgents.includes(agentIdOverride)) { 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.`, ); } } diff --git a/src/commands/agents.command-shared.ts b/src/commands/agents.command-shared.ts index ebb60e947..3c00d0e1b 100644 --- a/src/commands/agents.command-shared.ts +++ b/src/commands/agents.command-shared.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../cli/command-format.js"; import type { ClawdbotConfig } from "../config/config.js"; import { readConfigFileSnapshot } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -14,7 +15,7 @@ export async function requireValidConfig(runtime: RuntimeEnv): Promise `- ${issue.path}: ${issue.message}`).join("\n") : "Unknown validation issue."; 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); return null; } diff --git a/src/commands/agents.commands.list.ts b/src/commands/agents.commands.list.ts index edb18e92b..938663798 100644 --- a/src/commands/agents.commands.list.ts +++ b/src/commands/agents.commands.list.ts @@ -2,6 +2,7 @@ import type { AgentBinding } from "../config/types.js"; import { normalizeAgentId } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { describeBinding } from "./agents.bindings.js"; import { requireValidConfig } from "./agents.command-shared.js"; import type { AgentSummary } from "./agents.config.js"; @@ -116,7 +117,7 @@ export async function agentsListCommand( const lines = ["Agents:", ...summaries.map(formatSummary)]; lines.push("Routing rules map channel/account/peer to an agent. Use --bindings for full rules."); 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")); } diff --git a/src/commands/channels/shared.ts b/src/commands/channels/shared.ts index e360f774d..9191a7307 100644 --- a/src/commands/channels/shared.ts +++ b/src/commands/channels/shared.ts @@ -1,4 +1,5 @@ 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 { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.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") : "Unknown validation issue."; 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); return null; } diff --git a/src/commands/channels/status.ts b/src/commands/channels/status.ts index 521bf4c59..24c59fd14 100644 --- a/src/commands/channels/status.ts +++ b/src/commands/channels/status.ts @@ -8,6 +8,7 @@ import { formatAge } from "../../infra/channel-summary.js"; import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; import { formatDocsLink } from "../../terminal/links.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { theme } from "../../terminal/theme.js"; import { type ChatChannel, formatChannelAccountLabel, requireValidConfig } from "./shared.js"; @@ -142,7 +143,7 @@ export function formatGatewayChannelsStatusLines(payload: Record void; @@ -65,5 +66,5 @@ export async function buildGatewayInstallPlan(params: { export function gatewayInstallErrorHint(platform = process.platform): string { return platform === "win32" ? "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.`; } diff --git a/src/commands/docs.ts b/src/commands/docs.ts index 7172efee3..4ca479f65 100644 --- a/src/commands/docs.ts +++ b/src/commands/docs.ts @@ -3,6 +3,7 @@ import { runCommandWithTimeout } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.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_TIMEOUT_MS = 30_000; @@ -150,10 +151,10 @@ export async function docsSearchCommand(queryParts: string[], runtime: RuntimeEn const docs = formatDocsLink("/", "docs.clawd.bot"); if (isRich()) { 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 { runtime.log("Docs: https://docs.clawd.bot/"); - runtime.log('Search: clawdbot docs "your query"'); + runtime.log(`Search: ${formatCliCommand('clawdbot docs "your query"')}`); } return; } diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index 71430dee7..7fc17e28f 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -13,6 +13,7 @@ import { } from "../agents/auth-profiles.js"; import type { ClawdbotConfig } from "../config/config.js"; import { note } from "../terminal/note.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; export async function maybeRepairAnthropicOAuthProfileId( @@ -49,9 +50,9 @@ function formatAuthIssueHint(issue: AuthIssue): string | null { return "Run `claude setup-token` on the gateway host."; } 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 { diff --git a/src/commands/doctor-config-flow.ts b/src/commands/doctor-config-flow.ts index 27f7d6bc1..13b07af67 100644 --- a/src/commands/doctor-config-flow.ts +++ b/src/commands/doctor-config-flow.ts @@ -8,6 +8,7 @@ import { readConfigFileSnapshot, } from "../config/config.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { note } from "../terminal/note.js"; import { normalizeLegacyConfigValues } from "./doctor-legacy-config.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 (migrated) cfg = migrated; } 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) { cfg = normalized.config; } 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) { cfg = autoEnable.config; } else { - note('Run "clawdbot doctor --fix" to apply these changes.', "Doctor"); + note(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply these changes.`, "Doctor"); } } diff --git a/src/commands/doctor-format.ts b/src/commands/doctor-format.ts index 2b120b146..535937e10 100644 --- a/src/commands/doctor-format.ts +++ b/src/commands/doctor-format.ts @@ -8,6 +8,7 @@ import { isSystemdUnavailableDetail, renderSystemdUnavailableHints, } from "../daemon/systemd-hints.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { isWSLEnv } from "../infra/wsl.js"; import type { GatewayServiceRuntime } from "../daemon/service-runtime.js"; import { getResolvedLoggerSettings } from "../logging.js"; @@ -69,10 +70,10 @@ export function buildGatewayRuntimeHints( hints.push( `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) { - 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}`); return hints; } diff --git a/src/commands/doctor-gateway-daemon-flow.ts b/src/commands/doctor-gateway-daemon-flow.ts index 57196c0c6..46e4ce829 100644 --- a/src/commands/doctor-gateway-daemon-flow.ts +++ b/src/commands/doctor-gateway-daemon-flow.ts @@ -17,6 +17,7 @@ import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js"; import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js"; import { isWSL } from "../infra/wsl.js"; import type { RuntimeEnv } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { note } from "../terminal/note.js"; import { sleep } from "../utils.js"; import { @@ -201,7 +202,7 @@ export async function maybeRepairGatewayDaemon(params: { if (process.platform === "darwin") { const label = resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE); 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", ); } diff --git a/src/commands/doctor-security.ts b/src/commands/doctor-security.ts index 565575f5c..b3d82247f 100644 --- a/src/commands/doctor-security.ts +++ b/src/commands/doctor-security.ts @@ -4,10 +4,11 @@ import type { ChannelId } from "../channels/plugins/types.js"; import type { ClawdbotConfig } from "../config/config.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { note } from "../terminal/note.js"; +import { formatCliCommand } from "../cli/command-format.js"; export async function noteSecurityWarnings(cfg: ClawdbotConfig) { const warnings: string[] = []; - const auditHint = `- Run: clawdbot security audit --deep`; + const auditHint = `- Run: ${formatCliCommand("clawdbot security audit --deep")}`; const warnDmPolicy = async (params: { label: string; diff --git a/src/commands/doctor-update.ts b/src/commands/doctor-update.ts index 9d6b69f1c..0dd832626 100644 --- a/src/commands/doctor-update.ts +++ b/src/commands/doctor-update.ts @@ -3,6 +3,7 @@ import { isTruthyEnvValue } from "../infra/env.js"; import { runCommandWithTimeout } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; import { note } from "../terminal/note.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { DoctorOptions } from "./doctor-prompter.js"; async function detectClawdbotGitCheckout(root: string): Promise<"git" | "not-git" | "unknown"> { @@ -71,7 +72,7 @@ export async function maybeOfferUpdateBeforeDoctor(params: { note( [ "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"), "Update", ); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index c2f9ad1ba..ce589eecc 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -9,6 +9,7 @@ import { resolveConfiguredModelRef, resolveHooksGmailModel, } from "../agents/model-selection.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { ClawdbotConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, writeConfigFile } from "../config/config.js"; import { resolveGatewayService } from "../daemon/service.js"; @@ -258,7 +259,7 @@ export async function doctorCommand( runtime.log(`Backup: ${backupPath}`); } } else { - runtime.log('Run "clawdbot doctor --fix" to apply changes.'); + runtime.log(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply changes.`); } if (options.workspaceSuggestions !== false) { diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index 1ff30cd5f..8a322d2cd 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -15,6 +15,7 @@ import { } from "../../agents/agent-scope.js"; import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; import { parseDurationMs } from "../../cli/parse-duration.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, @@ -340,7 +341,9 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim const providers = resolvePluginProviders({ config, workspaceDir }); 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(); diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index ed84930a9..7ba356771 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -22,6 +22,7 @@ import { } from "../../infra/provider-usage.js"; import type { RuntimeEnv } from "../../runtime.js"; import { colorize, theme } from "../../terminal/theme.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { shortenHomePath } from "../../utils.js"; import { resolveProviderAuthOverview } from "./list.auth-overview.js"; import { isRich } from "./list.format.js"; @@ -395,8 +396,8 @@ export async function modelsStatusCommand( for (const provider of missingProvidersInUse) { const hint = provider === "anthropic" - ? "Run `claude setup-token` or `clawdbot configure`." - : "Run `clawdbot configure` or set an API key env var."; + ? `Run \`claude setup-token\` or \`${formatCliCommand("clawdbot configure")}\`.` + : `Run \`${formatCliCommand("clawdbot configure")}\` or set an API key env var.`; runtime.log(`- ${theme.heading(provider)} ${hint}`); } } diff --git a/src/commands/onboard-channels.ts b/src/commands/onboard-channels.ts index 744100e8b..945ecf3e2 100644 --- a/src/commands/onboard-channels.ts +++ b/src/commands/onboard-channels.ts @@ -14,6 +14,7 @@ import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { enablePluginInConfig } from "../plugins/enable.js"; import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js"; import type { ChannelChoice } from "./onboard-types.js"; @@ -186,7 +187,7 @@ async function noteChannelPrimer( await prompter.note( [ "DM security: default is pairing; unknown DMs get a pairing code.", - "Approve with: clawdbot pairing approve ", + `Approve with: ${formatCliCommand("clawdbot pairing approve ")}`, 'Public DMs require dmPolicy="open" + allowFrom=["*"].', 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.', `Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`, @@ -233,7 +234,7 @@ async function maybeConfigureDmPolicies(params: { await prompter.note( [ "Default: pairing (unknown DMs get a pairing code).", - `Approve: clawdbot pairing approve ${policy.channel} `, + `Approve: ${formatCliCommand(`clawdbot pairing approve ${policy.channel} `)}`, `Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`, `Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`, 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.', @@ -581,7 +582,7 @@ export async function setupChannels( { value: "__skip__", 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, diff --git a/src/commands/onboard-hooks.ts b/src/commands/onboard-hooks.ts index 0c2ac93ed..10cdf1293 100644 --- a/src/commands/onboard-hooks.ts +++ b/src/commands/onboard-hooks.ts @@ -3,6 +3,7 @@ import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { buildWorkspaceHookStatus } from "../hooks/hooks-status.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { formatCliCommand } from "../cli/command-format.js"; export async function setupInternalHooks( cfg: ClawdbotConfig, @@ -73,9 +74,9 @@ export async function setupInternalHooks( `Enabled ${selected.length} hook${selected.length > 1 ? "s" : ""}: ${selected.join(", ")}`, "", "You can manage hooks later with:", - " clawdbot hooks list", - " clawdbot hooks enable ", - " clawdbot hooks disable ", + ` ${formatCliCommand("clawdbot hooks list")}`, + ` ${formatCliCommand("clawdbot hooks enable ")}`, + ` ${formatCliCommand("clawdbot hooks disable ")}`, ].join("\n"), "Hooks Configured", ); diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index a59aef838..3d3e97749 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../cli/command-format.js"; import type { ClawdbotConfig } from "../config/config.js"; import { readConfigFileSnapshot } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -12,7 +13,9 @@ export async function runNonInteractiveOnboarding( ) { const snapshot = await readConfigFileSnapshot(); 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); return; } diff --git a/src/commands/onboard-non-interactive/local.ts b/src/commands/onboard-non-interactive/local.ts index 1d42a5faf..8d4088e46 100644 --- a/src/commands/onboard-non-interactive/local.ts +++ b/src/commands/onboard-non-interactive/local.ts @@ -1,6 +1,7 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { CONFIG_PATH_CLAWDBOT, resolveGatewayPort, writeConfigFile } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME } from "../daemon-runtime.js"; import { healthCommand } from "../health.js"; import { @@ -123,7 +124,7 @@ export async function runNonInteractiveOnboardingLocal(params: { if (!opts.json) { 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`, ); } } diff --git a/src/commands/onboard-non-interactive/remote.ts b/src/commands/onboard-non-interactive/remote.ts index b75d78d87..c28d96418 100644 --- a/src/commands/onboard-non-interactive/remote.ts +++ b/src/commands/onboard-non-interactive/remote.ts @@ -1,6 +1,7 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { applyWizardMetadata } from "../onboard-helpers.js"; import type { OnboardOptions } from "../onboard-types.js"; @@ -45,7 +46,7 @@ export async function runNonInteractiveOnboardingRemote(params: { runtime.log(`Remote gateway: ${remoteUrl}`); runtime.log(`Auth: ${payload.auth}`); 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`, ); } } diff --git a/src/commands/onboard-skills.ts b/src/commands/onboard-skills.ts index 448d5481e..ce3b06123 100644 --- a/src/commands/onboard-skills.ts +++ b/src/commands/onboard-skills.ts @@ -1,5 +1,6 @@ import { installSkill } from "../agents/skills-install.js"; import { buildWorkspaceSkillStatus } from "../agents/skills-status.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { ClawdbotConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; @@ -152,7 +153,9 @@ export async function setupSkills( spin.stop(`Install failed: ${name}${code}${detail ? ` — ${detail}` : ""}`); if (result.stderr) runtime.log(result.stderr.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"); } } diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index 2695c14ee..97e7bc3c7 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -6,6 +6,7 @@ import { resolveUserPath } from "../utils.js"; import { DEFAULT_WORKSPACE, handleReset } from "./onboard-helpers.js"; import { runInteractiveOnboarding } from "./onboard-interactive.js"; import { runNonInteractiveOnboarding } from "./onboard-non-interactive.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { OnboardOptions } from "./onboard-types.js"; 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.", "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"), ); runtime.exit(1); diff --git a/src/commands/reset.ts b/src/commands/reset.ts index c5aef6845..ed128cb5c 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -10,6 +10,7 @@ import { import { resolveGatewayService } from "../daemon/service.js"; import type { RuntimeEnv } from "../runtime.js"; import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { collectWorkspaceDirs, isPathWithin, @@ -143,7 +144,7 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) { for (const dir of sessionDirs) { await removePath(dir, runtime, { dryRun, label: dir }); } - runtime.log("Next: clawdbot onboard --install-daemon"); + runtime.log(`Next: ${formatCliCommand("clawdbot onboard --install-daemon")}`); return; } @@ -158,7 +159,7 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) { for (const workspace of workspaceDirs) { await removePath(workspace, runtime, { dryRun, label: workspace }); } - runtime.log("Next: clawdbot onboard --install-daemon"); + runtime.log(`Next: ${formatCliCommand("clawdbot onboard --install-daemon")}`); return; } } diff --git a/src/commands/sandbox-display.ts b/src/commands/sandbox-display.ts index 46647c6e9..76933ef27 100644 --- a/src/commands/sandbox-display.ts +++ b/src/commands/sandbox-display.ts @@ -3,6 +3,7 @@ */ import type { SandboxBrowserInfo, SandboxContainerInfo } from "../agents/sandbox.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { RuntimeEnv } from "../runtime.js"; import { formatAge, @@ -88,7 +89,9 @@ export function displaySummary( if (mismatchCount > 0) { 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.`, + ); } } diff --git a/src/commands/status-all.ts b/src/commands/status-all.ts index f65a3d164..d5f94b763 100644 --- a/src/commands/status-all.ts +++ b/src/commands/status-all.ts @@ -1,5 +1,6 @@ import { buildWorkspaceSkillStatus } from "../agents/skills-status.js"; import { withProgress } from "../cli/progress.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { loadConfig, readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js"; import { readLastGatewayErrorLine } from "../daemon/diagnostics.js"; import type { GatewayService } from "../daemon/service.js"; @@ -337,7 +338,7 @@ export async function statusAllCommand( Item: "Gateway", 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 ? { Item: "Gateway self", Value: gatewaySelfLine } : { Item: "Gateway self", Value: "unknown" }, diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index d87eb1817..f888bf9b8 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -7,6 +7,7 @@ import type { RuntimeEnv } from "../runtime.js"; import { runSecurityAudit } from "../security/audit.js"; import { renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { resolveMemoryCacheSummary, resolveMemoryFtsState, @@ -374,8 +375,8 @@ export async function statusCommand( runtime.log(theme.muted(`… +${sorted.length - shown.length} more`)); } } - runtime.log(theme.muted("Full report: clawdbot security audit")); - runtime.log(theme.muted("Deep probe: clawdbot security audit --deep")); + runtime.log(theme.muted(`Full report: ${formatCliCommand("clawdbot security audit")}`)); + runtime.log(theme.muted(`Deep probe: ${formatCliCommand("clawdbot security audit --deep")}`)); runtime.log(""); runtime.log(theme.heading("Channels")); @@ -531,11 +532,11 @@ export async function statusCommand( runtime.log(""); } runtime.log("Next steps:"); - runtime.log(" Need to share? clawdbot status --all"); - runtime.log(" Need to debug live? clawdbot logs --follow"); + runtime.log(` Need to share? ${formatCliCommand("clawdbot status --all")}`); + runtime.log(` Need to debug live? ${formatCliCommand("clawdbot logs --follow")}`); if (gatewayReachable) { - runtime.log(" Need to test channels? clawdbot status --deep"); + runtime.log(` Need to test channels? ${formatCliCommand("clawdbot status --deep")}`); } else { - runtime.log(" Fix reachability first: clawdbot gateway status"); + runtime.log(` Fix reachability first: ${formatCliCommand("clawdbot gateway status")}`); } } diff --git a/src/commands/status.update.ts b/src/commands/status.update.ts index f7db20c14..f3177e55b 100644 --- a/src/commands/status.update.ts +++ b/src/commands/status.update.ts @@ -4,6 +4,7 @@ import { compareSemverStrings, type UpdateCheckResult, } from "../infra/update-check.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { VERSION } from "../version.js"; export async function getUpdateCheckResult(params: { @@ -63,7 +64,7 @@ export function formatUpdateAvailableHint(update: UpdateCheckResult): string | n details.push(`npm ${availability.latestVersion}`); } 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 { diff --git a/src/daemon/systemd-hints.ts b/src/daemon/systemd-hints.ts index a393657ca..8499a718b 100644 --- a/src/daemon/systemd-hints.ts +++ b/src/daemon/systemd-hints.ts @@ -1,3 +1,5 @@ +import { formatCliCommand } from "../cli/command-format.js"; + export function isSystemdUnavailableDetail(detail?: string): boolean { if (!detail) return false; const normalized = detail.toLowerCase(); @@ -20,6 +22,6 @@ export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}): } return [ "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")}\`.`, ]; } diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index adcac10ff..dbd43d88f 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -13,7 +13,7 @@ import { applyMergePatch } from "../../config/merge-patch.js"; import { buildConfigSchema } from "../../config/schema.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { - DOCTOR_NONINTERACTIVE_HINT, + formatDoctorNonInteractiveHint, type RestartSentinelPayload, writeRestartSentinel, } from "../../infra/restart-sentinel.js"; @@ -336,7 +336,7 @@ export const configHandlers: GatewayRequestHandlers = { ts: Date.now(), sessionKey, message: note ?? null, - doctorHint: DOCTOR_NONINTERACTIVE_HINT, + doctorHint: formatDoctorNonInteractiveHint(), stats: { mode: "config.apply", root: CONFIG_PATH_CLAWDBOT, diff --git a/src/gateway/server-methods/update.ts b/src/gateway/server-methods/update.ts index 9e4717328..51b5ed790 100644 --- a/src/gateway/server-methods/update.ts +++ b/src/gateway/server-methods/update.ts @@ -1,7 +1,7 @@ import { resolveClawdbotPackageRoot } from "../../infra/clawdbot-root.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { - DOCTOR_NONINTERACTIVE_HINT, + formatDoctorNonInteractiveHint, type RestartSentinelPayload, writeRestartSentinel, } from "../../infra/restart-sentinel.js"; @@ -75,7 +75,7 @@ export const updateHandlers: GatewayRequestHandlers = { ts: Date.now(), sessionKey, message: note ?? null, - doctorHint: DOCTOR_NONINTERACTIVE_HINT, + doctorHint: formatDoctorNonInteractiveHint(), stats: { mode: result.mode, root: result.root ?? undefined, diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 9572b5a32..11da41275 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -4,6 +4,7 @@ import { registerSkillsChangeListener } from "../agents/skills/refresh.js"; import type { CanvasHostServer } from "../canvas-host/server.js"; import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js"; import { createDefaultDeps } from "../cli/deps.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { CONFIG_PATH_CLAWDBOT, isNixMode, @@ -155,7 +156,7 @@ export async function startGatewayServer( const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed); if (!migrated) { 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); @@ -177,7 +178,7 @@ export async function startGatewayServer( .join("\n") : "Unknown validation issue."; 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.`, ); } diff --git a/src/hooks/gmail-ops.ts b/src/hooks/gmail-ops.ts index 8d839f836..abff6ee76 100644 --- a/src/hooks/gmail-ops.ts +++ b/src/hooks/gmail-ops.ts @@ -11,6 +11,7 @@ import { } from "../config/config.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { buildDefaultHookUrl, buildGogWatchServeArgs, @@ -276,7 +277,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) { defaultRuntime.log(`- push endpoint: ${pushEndpoint}`); defaultRuntime.log(`- hook url: ${hookUrl}`); 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) { diff --git a/src/infra/outbound/targets.ts b/src/infra/outbound/targets.ts index 619b0d039..c31bfbc4b 100644 --- a/src/infra/outbound/targets.ts +++ b/src/infra/outbound/targets.ts @@ -1,4 +1,5 @@ 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 { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; @@ -109,7 +110,7 @@ export function resolveOutboundTarget(params: { return { ok: false, 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.`, ), }; } diff --git a/src/infra/ports-format.ts b/src/infra/ports-format.ts index 3b97c2368..6bf7db4bd 100644 --- a/src/infra/ports-format.ts +++ b/src/infra/ports-format.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../cli/command-format.js"; import type { PortListener, PortListenerKind, PortUsage } from "./ports-types.js"; export function classifyPortListener(listener: PortListener, port: number): PortListenerKind { @@ -20,7 +21,7 @@ export function buildPortHints(listeners: PortListener[], port: number): string[ const hints: string[] = []; if (kinds.has("gateway")) { 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")) { diff --git a/src/infra/restart-sentinel.ts b/src/infra/restart-sentinel.ts index 060bc3340..760554b03 100644 --- a/src/infra/restart-sentinel.ts +++ b/src/infra/restart-sentinel.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { formatCliCommand } from "../cli/command-format.js"; import { resolveStateDir } from "../config/paths.js"; export type RestartSentinelLog = { @@ -44,7 +45,11 @@ export type RestartSentinel = { const SENTINEL_FILENAME = "restart-sentinel.json"; -export const DOCTOR_NONINTERACTIVE_HINT = "Run: clawdbot doctor --non-interactive"; +export function formatDoctorNonInteractiveHint( + env: Record = process.env as Record, +): string { + return `Run: ${formatCliCommand("clawdbot doctor --non-interactive", env)}`; +} export function resolveRestartSentinelPath(env: NodeJS.ProcessEnv = process.env): string { return path.join(resolveStateDir(env), SENTINEL_FILENAME); diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index 4ab18570b..58d7b3f93 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -4,6 +4,7 @@ import { danger, info, logVerbose, shouldLogVerbose, warn } from "../globals.js" import { runExec } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { ensureBinary } from "./binaries.js"; function parsePossiblyNoisyJsonObject(stdout: string): Record { @@ -268,7 +269,7 @@ export async function ensureFunnel( runtime.error("Failed to enable Tailscale Funnel. Is it allowed on your tailnet?"); runtime.error( 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()) { diff --git a/src/infra/update-startup.ts b/src/infra/update-startup.ts index ffdce44e0..003a090c4 100644 --- a/src/infra/update-startup.ts +++ b/src/infra/update-startup.ts @@ -6,6 +6,7 @@ import { resolveStateDir } from "../config/paths.js"; import { resolveClawdbotPackageRoot } from "./clawdbot-root.js"; import { compareSemverStrings, fetchNpmTagVersion, checkUpdateStatus } from "./update-check.js"; import { VERSION } from "../version.js"; +import { formatCliCommand } from "../cli/command-format.js"; type UpdateCheckState = { lastCheckedAt?: string; @@ -102,7 +103,7 @@ export async function runGatewayUpdateCheck(params: { state.lastNotifiedVersion !== tagStatus.version || state.lastNotifiedTag !== tag; if (shouldNotify) { 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.lastNotifiedTag = tag; diff --git a/src/media/host.ts b/src/media/host.ts index b57dce82a..42f105d9c 100644 --- a/src/media/host.ts +++ b/src/media/host.ts @@ -3,6 +3,7 @@ import { ensurePortAvailable, PortInUseError } from "../infra/ports.js"; import { getTailnetHostname } from "../infra/tailscale.js"; import { logInfo } from "../logger.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { startMediaServer } from "./server.js"; import { saveMediaSource } from "./store.js"; @@ -36,7 +37,7 @@ export async function ensureMediaHosted( if (needsServerStart && !opts.startServer) { await fs.rm(saved.path).catch(() => {}); 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) { diff --git a/src/pairing/pairing-messages.ts b/src/pairing/pairing-messages.ts index ee593d904..c372af511 100644 --- a/src/pairing/pairing-messages.ts +++ b/src/pairing/pairing-messages.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../cli/command-format.js"; import type { PairingChannel } from "./pairing-store.js"; export function buildPairingReply(params: { @@ -14,6 +15,6 @@ export function buildPairingReply(params: { `Pairing code: ${code}`, "", "Ask the bot owner to approve with:", - `clawdbot pairing approve ${channel} `, + formatCliCommand(`clawdbot pairing approve ${channel} `), ].join("\n"); } diff --git a/src/providers/qwen-portal-oauth.ts b/src/providers/qwen-portal-oauth.ts index 88142656e..821059028 100644 --- a/src/providers/qwen-portal-oauth.ts +++ b/src/providers/qwen-portal-oauth.ts @@ -1,4 +1,5 @@ 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_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`; @@ -28,7 +29,7 @@ export async function refreshQwenPortalCredentials( const text = await response.text(); if (response.status === 400) { 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}`); diff --git a/src/security/audit-extra.ts b/src/security/audit-extra.ts index d30e7b178..95f06b89e 100644 --- a/src/security/audit-extra.ts +++ b/src/security/audit-extra.ts @@ -7,6 +7,7 @@ import type { ClawdbotConfig, ConfigFileSnapshot } from "../config/config.js"; import { createConfigIO } from "../config/config.js"; import { resolveNativeSkillsEnabled } from "../config/commands.js"; import { resolveOAuthDir } from "../config/paths.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js"; import { normalizeAgentId } from "../routing/session-key.js"; @@ -105,7 +106,7 @@ export function collectSyncedFolderFindings(params: { severity: "warn", 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.`, - 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; diff --git a/src/security/audit.ts b/src/security/audit.ts index b1db1610d..6ec2cd39d 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -5,6 +5,7 @@ import type { ClawdbotConfig } from "../config/config.js"; import { resolveBrowserConfig } from "../browser/config.js"; import { resolveConfigPath, resolveStateDir } from "../config/paths.js"; import { resolveGatewayAuth } from "../gateway/auth.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { probeGateway } from "../gateway/probe.js"; import { @@ -264,7 +265,7 @@ function collectBrowserControlFindings(cfg: ClawdbotConfig): SecurityAuditFindin severity: "warn", title: "Browser control config looks invalid", 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; } @@ -840,7 +841,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise", + formatCliCommand("clawdbot pairing approve telegram "), ].join("\n"), ); } diff --git a/src/web/active-listener.ts b/src/web/active-listener.ts index 82a07b56b..0147a25b6 100644 --- a/src/web/active-listener.ts +++ b/src/web/active-listener.ts @@ -1,3 +1,4 @@ +import { formatCliCommand } from "../cli/command-format.js"; import type { PollInput } from "../polls.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; if (!listener) { 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 }; diff --git a/src/web/auth-store.ts b/src/web/auth-store.ts index 9271f2e76..9b92e7703 100644 --- a/src/web/auth-store.ts +++ b/src/web/auth-store.ts @@ -7,6 +7,7 @@ import { info, success } from "../globals.js"; import { getChildLogger } from "../logging.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { WebChannel } from "../utils.js"; import { jidToE164, resolveUserPath } from "../utils.js"; @@ -176,7 +177,7 @@ export async function pickWebChannel( const hasWeb = await webAuthExists(authDir); if (!hasWeb) { throw new Error( - "No WhatsApp Web session found. Run `clawdbot channels login --channel whatsapp --verbose` to link.", + `No WhatsApp Web session found. Run \`${formatCliCommand("clawdbot channels login --channel whatsapp --verbose")}\` to link.`, ); } return choice; diff --git a/src/web/auto-reply/monitor.ts b/src/web/auto-reply/monitor.ts index b5f4383e0..bd0f8d201 100644 --- a/src/web/auto-reply/monitor.ts +++ b/src/web/auto-reply/monitor.ts @@ -11,6 +11,7 @@ import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejecti import { getChildLogger } from "../../logging.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { resolveWhatsAppAccount } from "../accounts.js"; import { setActiveWebListener } from "../active-listener.js"; import { monitorWebInbox } from "../inbound.js"; @@ -374,7 +375,7 @@ export async function monitorWebChannel( if (loggedOut) { runtime.error( - "WhatsApp session logged out. Run `clawdbot channels login --channel web` to relink.", + `WhatsApp session logged out. Run \`${formatCliCommand("clawdbot channels login --channel web")}\` to relink.`, ); await closeListener(); break; diff --git a/src/web/login.ts b/src/web/login.ts index 537a8de85..2571085c7 100644 --- a/src/web/login.ts +++ b/src/web/login.ts @@ -3,6 +3,7 @@ import { loadConfig } from "../config/config.js"; import { danger, info, success } from "../globals.js"; import { logInfo } from "../logger.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { resolveWhatsAppAccount } from "./accounts.js"; import { createWaSocket, formatError, logoutWeb, waitForWaConnection } from "./session.js"; @@ -56,7 +57,7 @@ export async function loginWeb( }); console.error( danger( - "WhatsApp reported the session is logged out. Cleared cached web session; please rerun clawdbot channels login and scan the QR again.", + `WhatsApp reported the session is logged out. Cleared cached web session; please rerun ${formatCliCommand("clawdbot channels login")} and scan the QR again.`, ), ); throw new Error("Session logged out; cache cleared. Re-run login."); diff --git a/src/web/session.ts b/src/web/session.ts index d193d4acf..2c957a829 100644 --- a/src/web/session.ts +++ b/src/web/session.ts @@ -12,6 +12,7 @@ import { danger, success } from "../globals.js"; import { getChildLogger, toPinoLikeLogger } from "../logging.js"; import { ensureDir, resolveUserPath } from "../utils.js"; import { VERSION } from "../version.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { maybeRestoreCredsFromBackup, @@ -134,7 +135,11 @@ export async function createWaSocket( if (connection === "close") { const status = getStatusCode(lastDisconnect?.error); if (status === DisconnectReason.loggedOut) { - console.error(danger("WhatsApp session logged out. Run: clawdbot channels login")); + console.error( + danger( + `WhatsApp session logged out. Run: ${formatCliCommand("clawdbot channels login")}`, + ), + ); } } if (connection === "open" && verbose) { diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index bca4b3e7c..717446017 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -17,6 +17,7 @@ import { waitForGatewayReachable, resolveControlUiLinks, } from "../commands/onboard-helpers.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { OnboardOptions } from "../commands/onboard-types.js"; import type { ClawdbotConfig } from "../config/config.js"; import { resolveGatewayService } from "../daemon/service.js"; @@ -396,7 +397,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption "Clawdbot uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search won’t work.", "", "Set it up interactively:", - "- Run: clawdbot configure --section web", + `- Run: ${formatCliCommand("clawdbot configure --section web")}`, "- Enable web_search and paste your Brave Search API key", "", "Alternative: set BRAVE_API_KEY in the Gateway environment (no config changes).", diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 948914dfd..96bc0058f 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -26,6 +26,7 @@ import type { OnboardOptions, ResetScope, } from "../commands/onboard-types.js"; +import { formatCliCommand } from "../cli/command-format.js"; import type { ClawdbotConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, @@ -97,7 +98,7 @@ export async function runOnboardingWizard( if (!snapshot.valid) { await prompter.outro( - "Config invalid. Run `clawdbot doctor` to repair it, then re-run onboarding.", + `Config invalid. Run \`${formatCliCommand("clawdbot doctor")}\` to repair it, then re-run onboarding.`, ); runtime.exit(1); return; @@ -133,7 +134,7 @@ export async function runOnboardingWizard( } } - const quickstartHint = "Configure details later via clawdbot configure."; + const quickstartHint = `Configure details later via ${formatCliCommand("clawdbot configure")}.`; const advancedHint = "Configure port, network, Tailscale, and auth options."; const explicitFlowRaw = opts.flow?.trim(); if (explicitFlowRaw && explicitFlowRaw !== "quickstart" && explicitFlowRaw !== "advanced") {