refactor: normalize cli command hints

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

View File

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

View File

@@ -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).

View File

@@ -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 dont 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

View File

@@ -378,7 +378,7 @@ clawdbot channels login
### Build errors on `main` — whats 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 youre in the repo root
pnpm install
pnpm build
pnpm clawdbot doctor
clawdbot doctor
clawdbot daemon restart
```

View File

@@ -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 dont have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
Multi-instance quickstart (optional):
```bash

View File

@@ -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`, its 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 cant find global npm binaries (including `clawdbot`).
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
```bash
export PATH="/path/from/npm/bin/-g:$PATH"
```
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
## Recommended (installer script)
## 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 dont have a global install yet, run repo commands via `pnpm clawdbot ...`.
### 4) Other install options
- Docker: [Docker](/install/docker)
- Nix: [Nix](/install/nix)
- Ansible: [Ansible](/install/ansible)
- Bun (CLI only): [Bun](/install/bun)
## After install
- Run onboarding: `clawdbot onboard --install-daemon`
- Quick check: `clawdbot doctor`
- Check gateway health: `clawdbot status` + `clawdbot health`
- Open the dashboard: `clawdbot dashboard`
## Install method: npm vs git (installer)
The installer supports two methods:
@@ -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 cant 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)

View File

@@ -118,7 +118,7 @@ Remove it with `npm rm -g clawdbot` (or `pnpm remove -g` / `bun remove -g` if yo
### Source checkout (git clone)
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.

View File

@@ -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.

View File

@@ -155,7 +155,7 @@ Options:
- `--timeout <ms>`: 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 apps discovery pipeline (NWBrowser + tailnet DNSSD fallback) differs from
the Node CLIs `dns-sd` based discovery.

View File

@@ -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)

View File

@@ -184,22 +184,25 @@ Clawdbot is a personal AI assistant you run on your own devices. It replies on t
The repo recommends running from source and using the onboarding wizard:
```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 dont 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 didnt launch, copy/paste the printed URL on the same machine. Tokens stay local to your host—nothing is fetched from the browser.
@@ -330,7 +333,7 @@ git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm build
pnpm clawdbot doctor
clawdbot doctor
clawdbot daemon restart
```

View File

@@ -116,6 +116,13 @@ If a token is configured, paste it into the Control UI settings (stored as `conn
⚠️ **Bun warning (WhatsApp + Telegram):** Bun has known issues with these
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 dont 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 wont be able to respond without it.
If `clawdbot health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent wont be able to respond without it.
Tip: `clawdbot status --all` is the best pasteable, read-only debug report.
Health probes: `clawdbot health` (or `clawdbot status --deep`) asks the running gateway for a health snapshot.

View File

@@ -35,9 +35,11 @@ clawdbot setup
From inside this repo, use the local CLI entry:
```bash
pnpm clawdbot setup
clawdbot setup
```
If you dont have a global install yet, run it via `pnpm clawdbot setup`.
## Stable workflow (macOS app first)
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

View File

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

View File

@@ -1,3 +1,4 @@
import { formatCliCommand } from "../../cli/command-format.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { 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");
}

View File

@@ -4,6 +4,7 @@ import { type Api, getEnvApiKey, type Model } from "@mariozechner/pi-ai";
import type { ClawdbotConfig } from "../config/config.js";
import type { 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 <id>) or copy auth-profiles.json from the main agentDir.",
`Configure auth for this agent (${formatCliCommand("clawdbot agents add <id>")}) or copy auth-profiles.json from the main agentDir.`,
].join(" "),
);
}

View File

@@ -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<string |
function formatSandboxRecreateHint(params: { scope: SandboxConfig["scope"]; sessionKey: string }) {
if (params.scope === "session") {
return `clawdbot sandbox recreate --session ${params.sessionKey}`;
return formatCliCommand(`clawdbot sandbox recreate --session ${params.sessionKey}`);
}
if (params.scope === "agent") {
const agentId = resolveSandboxAgentId(params.sessionKey) ?? "main";
return `clawdbot sandbox recreate --agent ${agentId}`;
return formatCliCommand(`clawdbot sandbox recreate --agent ${agentId}`);
}
return "clawdbot sandbox recreate --all";
return formatCliCommand("clawdbot sandbox recreate --all");
}
export async function ensureSandboxContainer(params: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.<provider>");
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");
}

View File

@@ -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");
}

View File

@@ -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.<provider>");
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");
}

View File

@@ -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(

View File

@@ -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);

View File

@@ -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<ResolvedAccount>(params: {
}
export function formatPairingApproveHint(channelId: string): string {
return `Approve via: clawdbot pairing list ${channelId} / clawdbot pairing approve ${channelId} <code>`;
const listCmd = formatCliCommand(`clawdbot pairing list ${channelId}`);
const approveCmd = formatCliCommand(`clawdbot pairing approve ${channelId} <code>`);
return `Approve via: ${listCmd} / ${approveCmd}`;
}

View File

@@ -9,6 +9,7 @@ import {
resolveSignalAccount,
} 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",

View File

@@ -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<void> {
async function noteTelegramUserIdHelp(prompter: WizardPrompter): Promise<void> {
await prompter.note(
[
"1) DM your bot, then read from.id in `clawdbot logs --follow` (safest)",
`1) DM your bot, then read from.id in \`${formatCliCommand("clawdbot logs --follow")}\` (safest)`,
"2) Or call https://api.telegram.org/bot<bot_token>/getUpdates and read message.from.id",
"3) Third-party: DM @userinfobot or @getidsbot",
`Docs: ${formatDocsLink("/telegram")}`,

View File

@@ -7,6 +7,7 @@ import type { DmPolicy } from "../../../config/types.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import 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, {

View File

@@ -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.`,
});
}
}

View File

@@ -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"),
),

View File

@@ -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);
});

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

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

View File

@@ -5,6 +5,7 @@ import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
import { danger, info } from "../globals.js";
import { 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 || "<root>"}: ${issue.message}`);
}
defaultRuntime.error("Run `clawdbot doctor` to repair, then retry.");
defaultRuntime.error(`Run \`${formatCliCommand("clawdbot doctor")}\` to repair, then retry.`);
defaultRuntime.exit(1);
return snapshot;
}

View File

@@ -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;
}

View File

@@ -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": {

View File

@@ -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`);
}

View File

@@ -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);

View File

@@ -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")}`];
}
}

View File

@@ -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) {

View File

@@ -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") {

View File

@@ -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;
}

View File

@@ -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 <channel> <code> (or: clawdbot pairing approve --channel <channel> <code>)`,
`Usage: ${formatCliCommand("clawdbot pairing approve <channel> <code>")} (or: ${formatCliCommand("clawdbot pairing approve --channel <channel> <code>")})`,
);
}
if (opts.channel && code != null) {
throw new Error(
`Too many arguments. Use: clawdbot pairing approve --channel <channel> <code>`,
`Too many arguments. Use: ${formatCliCommand("clawdbot pairing approve --channel <channel> <code>")}`,
);
}
const channel = parseChannel(channelRaw, channels);

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

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

View File

@@ -1,5 +1,6 @@
import path from "node:path";
import { 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",
);
});
});

View File

@@ -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 };

View File

@@ -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);
}

View File

@@ -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 (

View File

@@ -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,
);
}

View File

@@ -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<void> {
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<void> {
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<void> {
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.`,
),
);
}
}

View File

@@ -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.`,
);
}
}

View File

@@ -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.`,
);
}
}

View File

@@ -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<ClawdbotC
? 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;
}

View File

@@ -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"));
}

View File

@@ -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;
}

View File

@@ -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<string, unknown
`- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
);
}
lines.push(`- Run: clawdbot doctor`);
lines.push(`- Run: ${formatCliCommand("clawdbot doctor")}`);
lines.push("");
}
lines.push(

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import {
resolveSystemNodeInfo,
} from "../daemon/runtime-paths.js";
import { buildServiceEnvironment } from "../daemon/service-env.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
type WarnFn = (message: string, title?: string) => 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.`;
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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");
}
}

View File

@@ -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;
}

View File

@@ -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",
);
}

View File

@@ -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;

View File

@@ -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",
);

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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}`);
}
}

View File

@@ -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 <channel> <code>",
`Approve with: ${formatCliCommand("clawdbot pairing approve <channel> <code>")}`,
'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} <code>`,
`Approve: ${formatCliCommand(`clawdbot pairing approve ${policy.channel} <code>`)}`,
`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,

View File

@@ -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 <name>",
" clawdbot hooks disable <name>",
` ${formatCliCommand("clawdbot hooks list")}`,
` ${formatCliCommand("clawdbot hooks enable <name>")}`,
` ${formatCliCommand("clawdbot hooks disable <name>")}`,
].join("\n"),
"Hooks Configured",
);

View File

@@ -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;
}

View File

@@ -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`,
);
}
}

View File

@@ -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`,
);
}
}

View File

@@ -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");
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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.`,
);
}
}

View File

@@ -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" },

View File

@@ -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")}`);
}
}

View File

@@ -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 {

View File

@@ -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")}\`.`,
];
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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.`,
);
}

View File

@@ -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) {

View File

@@ -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.`,
),
};
}

View File

@@ -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")) {

View File

@@ -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<string, string | undefined> = process.env as Record<string, string | undefined>,
): string {
return `Run: ${formatCliCommand("clawdbot doctor --non-interactive", env)}`;
}
export function resolveRestartSentinelPath(env: NodeJS.ProcessEnv = process.env): string {
return path.join(resolveStateDir(env), SENTINEL_FILENAME);

View File

@@ -4,6 +4,7 @@ import { danger, info, logVerbose, shouldLogVerbose, warn } from "../globals.js"
import { runExec } from "../process/exec.js";
import { 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<string, unknown> {
@@ -268,7 +269,7 @@ export async function ensureFunnel(
runtime.error("Failed to enable Tailscale Funnel. Is it allowed on your tailnet?");
runtime.error(
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()) {

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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} <code>`,
formatCliCommand(`clawdbot pairing approve ${channel} <code>`),
].join("\n");
}

View File

@@ -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}`);

View File

@@ -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;

View File

@@ -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<Secu
severity: "warn",
title: "Gateway probe failed (deep)",
detail: deep.gateway.error ?? "gateway unreachable",
remediation: `Run "clawdbot status --all" to debug connectivity/auth, then re-run "clawdbot security audit --deep".`,
remediation: `Run "${formatCliCommand("clawdbot status --all")}" to debug connectivity/auth, then re-run "${formatCliCommand("clawdbot security audit --deep")}".`,
});
}

View File

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

View File

@@ -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 };

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